[8.15] [Inference Endpoints View] Deletion, search and filtering of inference endpoints (#186206) (#187887)

# Backport

This will backport the following commits from `main` to `8.15`:
- [[Inference Endpoints View] Deletion, search and filtering of
inference endpoints
(#186206)](https://github.com/elastic/kibana/pull/186206)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Saikat
Sarkar","email":"132922331+saikatsarkar056@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-07-09T14:42:52Z","message":"[Inference
Endpoints View] Deletion, search and filtering of inference endpoints
(#186206)\n\nThis PR consists of the following changes:\r\n- An option
to delete an existing inference endpoint\r\n- Filtering the endpoints
based on 'provider' and 'type'\r\n- Search option\r\n- Display the
trained models deployment status\r\n- Display additional 3rd party
providers (Mistral, Azure OpenAI, Azure\r\nAI Studio)\r\n- Add licensing
for gating enterprise licensed users\r\n\r\n### Stack
Management\r\n![Screenshot 2024-06-24 at 2
38\r\n44 PM](d8072069-2309-40b9-a723-6b34f64b7ef0)\r\n\r\n\r\n\r\n###
Serverless\r\n![Screenshot 2024-06-24 at 2
43\r\n36 PM](fe5be2fd-d9ca-41f7-b246-8767e88d2938)\r\n\r\n---------\r\n\r\nCo-authored-by:
Liam Thompson
<32779855+leemthompo@users.noreply.github.com>","sha":"ff651f20d247f2ccf64b712131edd346f3ccf1a8","branchLabelMapping":{"^v8.16.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:EnterpriseSearch","v8.15.0","v8.16.0"],"number":186206,"url":"https://github.com/elastic/kibana/pull/186206","mergeCommit":{"message":"[Inference
Endpoints View] Deletion, search and filtering of inference endpoints
(#186206)\n\nThis PR consists of the following changes:\r\n- An option
to delete an existing inference endpoint\r\n- Filtering the endpoints
based on 'provider' and 'type'\r\n- Search option\r\n- Display the
trained models deployment status\r\n- Display additional 3rd party
providers (Mistral, Azure OpenAI, Azure\r\nAI Studio)\r\n- Add licensing
for gating enterprise licensed users\r\n\r\n### Stack
Management\r\n![Screenshot 2024-06-24 at 2
38\r\n44 PM](d8072069-2309-40b9-a723-6b34f64b7ef0)\r\n\r\n\r\n\r\n###
Serverless\r\n![Screenshot 2024-06-24 at 2
43\r\n36 PM](fe5be2fd-d9ca-41f7-b246-8767e88d2938)\r\n\r\n---------\r\n\r\nCo-authored-by:
Liam Thompson
<32779855+leemthompo@users.noreply.github.com>","sha":"ff651f20d247f2ccf64b712131edd346f3ccf1a8"}},"sourceBranch":"main","suggestedTargetBranches":["8.15"],"targetPullRequestStates":[{"branch":"8.15","label":"v8.15.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.16.0","labelRegex":"^v8.16.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/186206","number":186206,"mergeCommit":{"message":"[Inference
Endpoints View] Deletion, search and filtering of inference endpoints
(#186206)\n\nThis PR consists of the following changes:\r\n- An option
to delete an existing inference endpoint\r\n- Filtering the endpoints
based on 'provider' and 'type'\r\n- Search option\r\n- Display the
trained models deployment status\r\n- Display additional 3rd party
providers (Mistral, Azure OpenAI, Azure\r\nAI Studio)\r\n- Add licensing
for gating enterprise licensed users\r\n\r\n### Stack
Management\r\n![Screenshot 2024-06-24 at 2
38\r\n44 PM](d8072069-2309-40b9-a723-6b34f64b7ef0)\r\n\r\n\r\n\r\n###
Serverless\r\n![Screenshot 2024-06-24 at 2
43\r\n36 PM](fe5be2fd-d9ca-41f7-b246-8767e88d2938)\r\n\r\n---------\r\n\r\nCo-authored-by:
Liam Thompson
<32779855+leemthompo@users.noreply.github.com>","sha":"ff651f20d247f2ccf64b712131edd346f3ccf1a8"}}]}]
BACKPORT-->
This commit is contained in:
Saikat Sarkar 2024-07-09 12:10:54 -06:00 committed by GitHub
parent 2193e948b6
commit ee7fd95214
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
70 changed files with 2455 additions and 201 deletions

View file

@ -8,7 +8,7 @@
export const ENTERPRISE_SEARCH_APP_ID = 'enterpriseSearch';
export const ENTERPRISE_SEARCH_CONTENT_APP_ID = 'enterpriseSearchContent';
export const ENTERPRISE_SEARCH_INFERENCE_ENDPOINTS_APP_ID = 'enterpriseSearchInferenceEndpoints';
export const ENTERPRISE_SEARCH_RELEVANCE_APP_ID = 'enterpriseSearchRelevance';
export const ENTERPRISE_SEARCH_APPLICATIONS_APP_ID = 'enterpriseSearchApplications';
export const ENTERPRISE_SEARCH_ANALYTICS_APP_ID = 'enterpriseSearchAnalytics';
export const ENTERPRISE_SEARCH_APPSEARCH_APP_ID = 'appSearch';

View file

@ -12,6 +12,7 @@ import {
ENTERPRISE_SEARCH_APP_ID,
ENTERPRISE_SEARCH_CONTENT_APP_ID,
ENTERPRISE_SEARCH_APPLICATIONS_APP_ID,
ENTERPRISE_SEARCH_RELEVANCE_APP_ID,
ENTERPRISE_SEARCH_ANALYTICS_APP_ID,
ENTERPRISE_SEARCH_APPSEARCH_APP_ID,
ENTERPRISE_SEARCH_WORKPLACESEARCH_APP_ID,
@ -23,6 +24,7 @@ import {
export type EnterpriseSearchApp = typeof ENTERPRISE_SEARCH_APP_ID;
export type EnterpriseSearchContentApp = typeof ENTERPRISE_SEARCH_CONTENT_APP_ID;
export type EnterpriseSearchApplicationsApp = typeof ENTERPRISE_SEARCH_APPLICATIONS_APP_ID;
export type EnterpriseSearchRelevanceApp = typeof ENTERPRISE_SEARCH_RELEVANCE_APP_ID;
export type EnterpriseSearchAnalyticsApp = typeof ENTERPRISE_SEARCH_ANALYTICS_APP_ID;
export type EnterpriseSearchAppsearchApp = typeof ENTERPRISE_SEARCH_APPSEARCH_APP_ID;
export type EnterpriseSearchWorkplaceSearchApp = typeof ENTERPRISE_SEARCH_WORKPLACESEARCH_APP_ID;
@ -38,10 +40,13 @@ export type ApplicationsLinkId = 'searchApplications' | 'playground';
export type AppsearchLinkId = 'engines';
export type RelevanceLinkId = 'inferenceEndpoints';
export type DeepLinkId =
| EnterpriseSearchApp
| EnterpriseSearchContentApp
| EnterpriseSearchApplicationsApp
| EnterpriseSearchRelevanceApp
| EnterpriseSearchAnalyticsApp
| EnterpriseSearchAppsearchApp
| EnterpriseSearchWorkplaceSearchApp
@ -52,4 +57,5 @@ export type DeepLinkId =
| SearchHomepage
| `${EnterpriseSearchContentApp}:${ContentLinkId}`
| `${EnterpriseSearchApplicationsApp}:${ApplicationsLinkId}`
| `${EnterpriseSearchAppsearchApp}:${AppsearchLinkId}`;
| `${EnterpriseSearchAppsearchApp}:${AppsearchLinkId}`
| `${EnterpriseSearchRelevanceApp}:${RelevanceLinkId}`;

View file

@ -9,7 +9,7 @@
export {
ENTERPRISE_SEARCH_APP_ID,
ENTERPRISE_SEARCH_CONTENT_APP_ID,
ENTERPRISE_SEARCH_INFERENCE_ENDPOINTS_APP_ID,
ENTERPRISE_SEARCH_RELEVANCE_APP_ID,
ENTERPRISE_SEARCH_APPLICATIONS_APP_ID,
ENTERPRISE_SEARCH_ANALYTICS_APP_ID,
ENTERPRISE_SEARCH_APPSEARCH_APP_ID,

View file

@ -135,7 +135,7 @@ export const applicationUsageSchema = {
canvas: commonSchema,
enterpriseSearch: commonSchema,
enterpriseSearchContent: commonSchema,
enterpriseSearchInferenceEndpoints: commonSchema,
enterpriseSearchRelevance: commonSchema,
enterpriseSearchAnalytics: commonSchema,
enterpriseSearchApplications: commonSchema,
enterpriseSearchAISearch: commonSchema,

View file

@ -2098,7 +2098,7 @@
}
}
},
"enterpriseSearchInferenceEndpoints": {
"enterpriseSearchRelevance": {
"properties": {
"appId": {
"type": "keyword",

View file

@ -201,6 +201,52 @@ export type InferenceServiceSettings =
api_key: string;
organization_id: string;
url: string;
model_id: string;
};
}
| {
service: 'mistral';
service_settings: {
api_key: string;
model: string;
max_input_tokens: string;
rate_limit: {
requests_per_minute: number;
};
};
}
| {
service: 'cohere';
service_settings: {
similarity: string;
dimensions: string;
model_id: string;
embedding_type: string;
};
}
| {
service: 'azureaistudio';
service_settings: {
target: string;
provider: string;
embedding_type: string;
};
}
| {
service: 'azureopenai';
service_settings: {
resource_name: string;
deployment_id: string;
api_version: string;
};
}
| {
service: 'googleaistudio';
service_settings: {
model_id: string;
rate_limit: {
requests_per_minute: number;
};
};
}
| {

View file

@ -8,7 +8,7 @@
import {
ENTERPRISE_SEARCH_APP_ID,
ENTERPRISE_SEARCH_CONTENT_APP_ID,
ENTERPRISE_SEARCH_INFERENCE_ENDPOINTS_APP_ID,
ENTERPRISE_SEARCH_RELEVANCE_APP_ID,
ENTERPRISE_SEARCH_APPLICATIONS_APP_ID,
ENTERPRISE_SEARCH_ANALYTICS_APP_ID,
ENTERPRISE_SEARCH_APPSEARCH_APP_ID,
@ -178,7 +178,7 @@ export const VECTOR_SEARCH_PLUGIN = {
};
export const INFERENCE_ENDPOINTS_PLUGIN = {
ID: ENTERPRISE_SEARCH_INFERENCE_ENDPOINTS_APP_ID,
ID: ENTERPRISE_SEARCH_RELEVANCE_APP_ID,
NAME: i18n.translate('xpack.enterpriseSearch.inferenceEndpoints.productName', {
defaultMessage: 'Inference Endpoints',
}),

View file

@ -10,6 +10,7 @@ import { licensingMock } from '@kbn/licensing-plugin/public/mocks';
export const mockLicensingValues = {
license: licensingMock.createLicense(),
hasPlatinumLicense: false,
hasEnterpriseLicense: true,
hasGoldLicense: false,
isTrial: false,
canManageLicense: true,

View file

@ -40,6 +40,8 @@ import {
import { INFERENCE_ENDPOINTS_PATH } from '../../enterprise_search_relevance/routes';
import { KibanaLogic } from '../kibana';
import { LicensingLogic } from '../licensing';
import { generateNavLink } from './nav_link_helpers';
/**
@ -51,7 +53,11 @@ import { generateNavLink } from './nav_link_helpers';
export const useEnterpriseSearchNav = (alwaysReturn = false) => {
const { isSearchHomepageEnabled, searchHomepage, isSidebarEnabled, productAccess } =
useValues(KibanaLogic);
const { hasEnterpriseLicense } = useValues(LicensingLogic);
const indicesNavItems = useIndicesNav();
if (!isSidebarEnabled && !alwaysReturn) return undefined;
const navItems: Array<EuiSideNavItemTypeEnhanced<unknown>> = [
@ -154,25 +160,29 @@ export const useEnterpriseSearchNav = (alwaysReturn = false) => {
defaultMessage: 'Build',
}),
},
{
id: 'relevance',
items: [
{
id: 'inference_endpoints',
name: i18n.translate('xpack.enterpriseSearch.nav.inferenceEndpointsTitle', {
defaultMessage: 'Inference Endpoints',
}),
...generateNavLink({
shouldNotCreateHref: true,
shouldShowActiveForSubroutes: true,
to: INFERENCE_ENDPOINTS_PLUGIN.URL + INFERENCE_ENDPOINTS_PATH,
}),
},
],
name: i18n.translate('xpack.enterpriseSearch.nav.relevanceTitle', {
defaultMessage: 'Relevance',
}),
},
...(hasEnterpriseLicense
? [
{
id: 'relevance',
items: [
{
id: 'inference_endpoints',
name: i18n.translate('xpack.enterpriseSearch.nav.inferenceEndpointsTitle', {
defaultMessage: 'Inference Endpoints',
}),
...generateNavLink({
shouldNotCreateHref: true,
shouldShowActiveForSubroutes: true,
to: INFERENCE_ENDPOINTS_PLUGIN.URL + INFERENCE_ENDPOINTS_PATH,
}),
},
],
name: i18n.translate('xpack.enterpriseSearch.nav.relevanceTitle', {
defaultMessage: 'Relevance',
}),
},
]
: []),
{
id: 'es_getting_started',
items: [

View file

@ -167,6 +167,38 @@ describe('LicensingLogic', () => {
});
});
describe('hasEnterpriseLicense', () => {
it('is true for enterprise and trial licenses', () => {
updateLicense({ status: 'active', type: 'enterprise' });
expect(LicensingLogic.values.hasEnterpriseLicense).toEqual(true);
updateLicense({ status: 'active', type: 'trial' });
expect(LicensingLogic.values.hasEnterpriseLicense).toEqual(true);
});
it('is false if the current license is expired', () => {
updateLicense({ status: 'expired', type: 'enterprise' });
expect(LicensingLogic.values.hasEnterpriseLicense).toEqual(false);
updateLicense({ status: 'expired', type: 'trial' });
expect(LicensingLogic.values.hasEnterpriseLicense).toEqual(false);
});
it('is false for licenses below enterprise', () => {
updateLicense({ status: 'active', type: 'gold' });
expect(LicensingLogic.values.hasEnterpriseLicense).toEqual(false);
updateLicense({ status: 'active', type: 'platinum' });
expect(LicensingLogic.values.hasEnterpriseLicense).toEqual(false);
updateLicense({ status: 'active', type: 'basic' });
expect(LicensingLogic.values.hasEnterpriseLicense).toEqual(false);
updateLicense({ status: 'active', type: 'standard' });
expect(LicensingLogic.values.hasEnterpriseLicense).toEqual(false);
});
});
describe('isTrial', () => {
it('is true for active trial license', () => {
updateLicense({ status: 'active', type: 'trial' });

View file

@ -13,6 +13,7 @@ import { ILicense } from '@kbn/licensing-plugin/public';
interface LicensingValues {
license: ILicense | null;
licenseSubscription: Subscription | null;
hasEnterpriseLicense: boolean;
hasPlatinumLicense: boolean;
hasGoldLicense: boolean;
isTrial: boolean;
@ -52,6 +53,13 @@ export const LicensingLogic = kea<MakeLogicType<LicensingValues, LicensingAction
return license?.isActive && qualifyingLicenses.includes(license?.type);
},
],
hasEnterpriseLicense: [
(selectors) => [selectors.license],
(license) => {
const qualifyingLicenses = ['enterprise', 'trial'];
return license?.isActive && qualifyingLicenses.includes(license?.type);
},
],
hasGoldLicense: [
(selectors) => [selectors.license],
(license) => {

View file

@ -211,7 +211,7 @@ export const getNavigationTreeDefinition = ({
}),
},
{
children: [{ link: 'searchInferenceEndpoints' }],
children: [{ link: 'enterpriseSearchRelevance:inferenceEndpoints' }],
id: 'relevance',
title: i18n.translate('xpack.enterpriseSearch.searchNav.relevance', {
defaultMessage: 'Relevance',

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { BehaviorSubject, firstValueFrom } from 'rxjs';
import { BehaviorSubject, firstValueFrom, Subscription } from 'rxjs';
import { ChartsPluginStart } from '@kbn/charts-plugin/public';
import { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public';
@ -27,6 +27,7 @@ import type { HomePublicPluginSetup } from '@kbn/home-plugin/public';
import { i18n } from '@kbn/i18n';
import type { IndexManagementPluginStart } from '@kbn/index-management';
import { LensPublicStart } from '@kbn/lens-plugin/public';
import { ILicense } from '@kbn/licensing-plugin/public';
import { LicensingPluginStart } from '@kbn/licensing-plugin/public';
import { MlPluginStart } from '@kbn/ml-plugin/public';
import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public';
@ -84,6 +85,7 @@ export type EnterpriseSearchPublicStart = ReturnType<EnterpriseSearchPlugin['sta
interface PluginsSetup {
cloud?: CloudSetup;
licensing: LicensingPluginStart;
home?: HomePublicPluginSetup;
searchHomepage?: SearchHomepagePluginSetup;
security?: SecurityPluginSetup;
@ -186,6 +188,7 @@ const appSearchLinks: AppDeepLink[] = [
export class EnterpriseSearchPlugin implements Plugin {
private config: ClientConfigType;
private licenseSubscription: Subscription | null = null;
constructor(initializerContext: PluginInitializerContext) {
this.config = initializerContext.config.get<ClientConfigType>();
@ -261,7 +264,8 @@ export class EnterpriseSearchPlugin implements Plugin {
if (!config.ui?.enabled) {
return;
}
const { cloud, share } = plugins;
const { cloud, share, licensing } = plugins;
const useSearchHomepage =
plugins.searchHomepage && plugins.searchHomepage.isHomepageFeatureEnabled();
@ -445,29 +449,33 @@ export class EnterpriseSearchPlugin implements Plugin {
title: ANALYTICS_PLUGIN.NAME,
});
core.application.register({
appRoute: INFERENCE_ENDPOINTS_PLUGIN.URL,
category: DEFAULT_APP_CATEGORIES.enterpriseSearch,
deepLinks: relevanceLinks,
euiIconType: INFERENCE_ENDPOINTS_PLUGIN.LOGO,
id: INFERENCE_ENDPOINTS_PLUGIN.ID,
mount: async (params: AppMountParameters) => {
const kibanaDeps = await this.getKibanaDeps(core, params, cloud);
const { chrome, http } = kibanaDeps.core;
chrome.docTitle.change(INFERENCE_ENDPOINTS_PLUGIN.NAME);
this.licenseSubscription = licensing?.license$.subscribe((license: ILicense) => {
if (license.isActive && license.hasAtLeast('enterprise')) {
core.application.register({
appRoute: INFERENCE_ENDPOINTS_PLUGIN.URL,
category: DEFAULT_APP_CATEGORIES.enterpriseSearch,
deepLinks: relevanceLinks,
euiIconType: INFERENCE_ENDPOINTS_PLUGIN.LOGO,
id: INFERENCE_ENDPOINTS_PLUGIN.ID,
mount: async (params: AppMountParameters) => {
const kibanaDeps = await this.getKibanaDeps(core, params, cloud);
const { chrome, http } = kibanaDeps.core;
chrome.docTitle.change(INFERENCE_ENDPOINTS_PLUGIN.NAME);
await this.getInitialData(http);
const pluginData = this.getPluginData();
await this.getInitialData(http);
const pluginData = this.getPluginData();
const { renderApp } = await import('./applications');
const { EnterpriseSearchRelevance } = await import(
'./applications/enterprise_search_relevance'
);
const { renderApp } = await import('./applications');
const { EnterpriseSearchRelevance } = await import(
'./applications/enterprise_search_relevance'
);
return renderApp(EnterpriseSearchRelevance, kibanaDeps, pluginData);
},
title: INFERENCE_ENDPOINTS_PLUGIN.NAME,
visibleIn: [],
return renderApp(EnterpriseSearchRelevance, kibanaDeps, pluginData);
},
title: INFERENCE_ENDPOINTS_PLUGIN.NAME,
visibleIn: [],
});
}
});
core.application.register({
@ -645,7 +653,9 @@ export class EnterpriseSearchPlugin implements Plugin {
return {};
}
public stop() {}
public stop() {
this.licenseSubscription?.unsubscribe();
}
private updateSideNavDefinition = (items: Partial<DynamicSideNavItems>) => {
this.sideNavDynamicItems$.next({ ...this.sideNavDynamicItems$.getValue(), ...items });

View file

@ -21,6 +21,7 @@ import { DataPluginStart } from '@kbn/data-plugin/server/plugin';
import { PluginSetupContract as FeaturesPluginSetup } from '@kbn/features-plugin/server';
import { GlobalSearchPluginSetup } from '@kbn/global-search-plugin/server';
import type { GuidedOnboardingPluginSetup } from '@kbn/guided-onboarding-plugin/server';
import type { LicensingPluginStart } from '@kbn/licensing-plugin/public';
import { LogsSharedPluginSetup } from '@kbn/logs-shared-plugin/server';
import type { MlPluginSetup } from '@kbn/ml-plugin/server';
import { SearchConnectorsPluginSetup } from '@kbn/search-connectors-plugin/server';
@ -95,6 +96,7 @@ interface PluginsSetup {
guidedOnboarding?: GuidedOnboardingPluginSetup;
logsShared: LogsSharedPluginSetup;
ml?: MlPluginSetup;
licensing: LicensingPluginStart;
searchConnectors?: SearchConnectorsPluginSetup;
security: SecurityPluginSetup;
usageCollection?: UsageCollectionSetup;
@ -148,6 +150,7 @@ export class EnterpriseSearchPlugin implements Plugin {
logsShared,
customIntegrations,
ml,
licensing,
guidedOnboarding,
cloud,
searchConnectors,
@ -262,6 +265,7 @@ export class EnterpriseSearchPlugin implements Plugin {
log,
enterpriseSearchRequestHandler,
ml,
licensing,
};
registerConfigDataRoute(dependencies);

View file

@ -14,6 +14,10 @@ export const INFERENCE_ENDPOINT_LABEL = i18n.translate(
}
);
export const CANCEL = i18n.translate('xpack.searchInferenceEndpoints.cancel', {
defaultMessage: 'Cancel',
});
export const MANAGE_INFERENCE_ENDPOINTS_LABEL = i18n.translate(
'xpack.searchInferenceEndpoints.allInferenceEndpoints.description',
{
@ -94,3 +98,63 @@ export const FORBIDDEN_TO_ACCESS_TRAINED_MODELS = i18n.translate(
defaultMessage: 'Forbidden to access trained models',
}
);
export const COPY_ID_ACTION_LABEL = i18n.translate(
'xpack.searchInferenceEndpoints.actions.copyID',
{
defaultMessage: 'Copy endpoint ID',
}
);
export const COPY_ID_ACTION_SUCCESS = i18n.translate(
'xpack.searchInferenceEndpoints.actions.copyIDSuccess',
{
defaultMessage: 'Inference endpoint ID copied!',
}
);
export const ENDPOINT_ADDED_SUCCESS = i18n.translate(
'xpack.searchInferenceEndpoints.actions.endpointAddedSuccess',
{
defaultMessage: 'Endpoint added',
}
);
export const ENDPOINT_CREATION_FAILED = i18n.translate(
'xpack.searchInferenceEndpoints.actions.endpointAddedFailure',
{
defaultMessage: 'Endpoint creation failed',
}
);
export const ENDPOINT_ADDED_SUCCESS_DESCRIPTION = (endpointId: string) =>
i18n.translate('xpack.searchInferenceEndpoints.actions.endpointAddedSuccessDescription', {
defaultMessage: 'The inference endpoint "{endpointId}" was added.',
values: { endpointId },
});
export const DELETE_ACTION_LABEL = i18n.translate(
'xpack.searchInferenceEndpoints.actions.deleteSingleEndpoint',
{
defaultMessage: 'Delete endpoint',
}
);
export const ENDPOINT = i18n.translate('xpack.searchInferenceEndpoints.endpoint', {
defaultMessage: 'Endpoint',
});
export const SERVICE_PROVIDER = i18n.translate('xpack.searchInferenceEndpoints.serviceProvider', {
defaultMessage: 'Service',
});
export const TASK_TYPE = i18n.translate('xpack.searchInferenceEndpoints.taskType', {
defaultMessage: 'Type',
});
export const TRAINED_MODELS_STAT_GATHER_FAILED = i18n.translate(
'xpack.searchInferenceEndpoints.actions.trainedModelsStatGatherFailed',
{
defaultMessage: 'Failed to retrieve trained model statistics',
}
);

View file

@ -7,6 +7,7 @@
export enum APIRoutes {
GET_INFERENCE_ENDPOINTS = '/internal/inference_endpoints/endpoints',
DELETE_INFERENCE_ENDPOINT = '/internal/inference_endpoint/endpoints/{type}/{id}',
}
export interface SearchInferenceEndpointsConfigType {

View file

@ -0,0 +1,44 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.7797 2.01892C21.612 2.01892 22.3496 2.64311 22.6144 3.55102C22.8793 4.45893 24.4302 10.0766 24.4302 10.0766V21.2364H18.8125L18.9261 2H20.7797V2.01892Z" fill="url(#paint0_linear_3625_43445)"/>
<path d="M29.0265 10.737C29.0265 10.3397 28.705 10.0371 28.3267 10.0371H25.0166C22.69 10.0371 20.7986 11.9286 20.7986 14.2551V21.2536H24.8084C27.1351 21.2536 29.0265 19.3621 29.0265 17.0356V10.737Z" fill="url(#paint1_linear_3625_43445)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.7798 2.01758C20.1367 2.01758 19.626 2.52828 19.626 3.17139L19.5126 24.4128C19.5126 27.5148 16.9969 30.0305 13.8949 30.0305H3.69968C3.2079 30.0305 2.88634 29.5576 3.03766 29.1037L11.2089 5.78164C12.0033 3.53077 14.1217 2.01758 16.505 2.01758H20.7987H20.7798Z" fill="url(#paint2_linear_3625_43445)"/>
<defs>
<linearGradient id="paint0_linear_3625_43445" x1="23.1251" y1="21.6525" x2="18.3964" y2="2.71876" gradientUnits="userSpaceOnUse">
<stop stop-color="#712575"/>
<stop offset="0.09" stop-color="#9A2884"/>
<stop offset="0.18" stop-color="#BF2C92"/>
<stop offset="0.27" stop-color="#DA2E9C"/>
<stop offset="0.34" stop-color="#EB30A2"/>
<stop offset="0.4" stop-color="#F131A5"/>
<stop offset="0.5" stop-color="#EC30A3"/>
<stop offset="0.61" stop-color="#DF2F9E"/>
<stop offset="0.72" stop-color="#C92D96"/>
<stop offset="0.83" stop-color="#AA2A8A"/>
<stop offset="0.95" stop-color="#83267C"/>
<stop offset="1" stop-color="#712575"/>
</linearGradient>
<linearGradient id="paint1_linear_3625_43445" x1="24.922" y1="2.41679" x2="24.922" y2="29.1246" gradientUnits="userSpaceOnUse">
<stop stop-color="#DA7ED0"/>
<stop offset="0.08" stop-color="#B17BD5"/>
<stop offset="0.19" stop-color="#8778DB"/>
<stop offset="0.3" stop-color="#6276E1"/>
<stop offset="0.41" stop-color="#4574E5"/>
<stop offset="0.54" stop-color="#2E72E8"/>
<stop offset="0.67" stop-color="#1D71EB"/>
<stop offset="0.81" stop-color="#1471EC"/>
<stop offset="1" stop-color="#1171ED"/>
</linearGradient>
<linearGradient id="paint2_linear_3625_43445" x1="23.3144" y1="3.02245" x2="5.61009" y2="31.4137" gradientUnits="userSpaceOnUse">
<stop stop-color="#DA7ED0"/>
<stop offset="0.05" stop-color="#B77BD4"/>
<stop offset="0.11" stop-color="#9079DA"/>
<stop offset="0.18" stop-color="#6E77DF"/>
<stop offset="0.25" stop-color="#5175E3"/>
<stop offset="0.33" stop-color="#3973E7"/>
<stop offset="0.42" stop-color="#2772E9"/>
<stop offset="0.54" stop-color="#1A71EB"/>
<stop offset="0.68" stop-color="#1371EC"/>
<stop offset="1" stop-color="#1171ED"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -0,0 +1,9 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 6.2V25.8C2 28.1193 3.88067 30 6.2 30H25.8C28.1193 30 30 28.1193 30 25.8V6.2C30 3.88067 28.1193 2 25.8 2H6.2C3.88067 2 2 3.88067 2 6.2ZM18.8 2V7.6C18.8 13.7849 23.8151 18.8 30 18.8H24.4C18.2151 18.8 13.2016 23.812 13.2 29.9969V24.4C13.2 18.2151 8.18489 13.2 2 13.2H7.6C13.7849 13.2 18.8 8.18489 18.8 2Z" fill="url(#paint0_radial_3625_43439)"/>
<defs>
<radialGradient id="paint0_radial_3625_43439" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(14.7834 15.2861) rotate(45) scale(17.5637 23.9043)">
<stop stop-color="#83B9F9"/>
<stop offset="1" stop-color="#0078D4"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 779 B

View file

@ -0,0 +1,9 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Frame 1502">
<g id="Group">
<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M4.69017 8.30813C5.05012 8.30813 5.76614 8.2878 6.75587 7.86999C7.9092 7.383 10.2038 6.49902 11.8591 5.59107C13.0168 4.95601 13.5242 4.11609 13.5242 2.98501C13.5243 1.41518 12.2833 0.142578 10.7525 0.142578H4.3387C2.13989 0.142578 0.357422 1.97049 0.357422 4.22534C0.357422 6.48016 2.02634 8.30813 4.69017 8.30813Z" fill="#343741"/>
<path id="Vector_2" fill-rule="evenodd" clip-rule="evenodd" d="M5.77539 11.1217C5.77539 10.0164 6.42425 9.01987 7.41974 8.59618L9.43959 7.73656C11.4826 6.86702 13.7314 8.40671 13.7314 10.6752C13.7314 12.4326 12.3419 13.8572 10.6281 13.8568L8.44111 13.8561C6.96877 13.8558 5.77539 12.6317 5.77539 11.1217Z" fill="#343741"/>
<path id="Vector_3" d="M2.65251 8.8457C1.38499 8.8457 0.357422 9.89945 0.357422 11.1993V11.5041C0.357422 12.8039 1.38495 13.8577 2.65247 13.8577C3.91999 13.8577 4.94757 12.8039 4.94757 11.5041V11.1993C4.94757 9.89945 3.92003 8.8457 2.65251 8.8457Z" fill="#343741"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,16 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Elastic Logo" clip-path="url(#clip0_3517_16106)">
<path id="Outline" fill-rule="evenodd" clip-rule="evenodd" d="M13.3683 5.76929C13.6877 6.22686 13.8583 6.77172 13.8569 7.32972C13.8541 7.88997 13.6804 8.43603 13.3589 8.89486C13.038 9.35285 12.5845 9.7015 12.0595 9.89386C12.2124 10.3141 12.2253 10.7725 12.0961 11.2006C11.9669 11.6287 11.7027 12.0035 11.3429 12.269C10.9833 12.5333 10.5474 12.6731 10.1011 12.6674C9.65492 12.6617 9.22272 12.5107 8.87003 12.2373C8.39436 12.9043 7.71899 13.4029 6.94145 13.661C6.16465 13.9188 5.32592 13.9222 4.54703 13.6709C3.76743 13.4189 3.08814 12.9255 2.60731 12.2621C2.12537 11.5976 1.86627 10.7975 1.86717 9.97657C1.86706 9.72855 1.89001 9.48106 1.93574 9.23729C1.40909 9.04854 0.954014 8.70098 0.633311 8.24258C0.311933 7.78314 0.140528 7.23554 0.142597 6.67486C0.145501 6.11457 0.319395 5.56851 0.641026 5.10972C0.962305 4.6517 1.41627 4.30319 1.94174 4.11115C1.78662 3.69074 1.77202 3.23136 1.90014 2.80195C2.02826 2.37254 2.29219 1.99626 2.65231 1.72958C3.01211 1.46404 3.44878 1.32328 3.89592 1.32869C4.34306 1.33411 4.77619 1.4854 5.12945 1.75958C5.64444 1.04047 6.39005 0.519302 7.24231 0.282718C8.09391 0.0467746 9.00068 0.110161 9.81117 0.46229C10.6227 0.815129 11.2891 1.43484 11.6999 2.21858C12.1115 3.00344 12.2435 3.9051 12.074 4.775C12.5977 4.96493 13.0498 5.31224 13.3683 5.76929ZM5.53403 6.05129L8.53488 7.42743L11.5632 4.76472C11.607 4.54501 11.6289 4.32148 11.6283 4.09743C11.6286 3.37229 11.3974 2.666 10.9683 2.08143C10.5403 1.49792 9.93643 1.06698 9.24546 0.851861C8.55514 0.637319 7.81416 0.650239 7.13174 0.888718C6.44837 1.12763 5.85956 1.5793 5.45174 2.17743L4.9486 4.79943L5.53445 6.05086L5.53403 6.05129ZM2.42817 9.24029C2.33332 9.70704 2.33721 10.1885 2.43958 10.6536C2.54196 11.1188 2.74055 11.5574 3.0226 11.9411C3.453 12.5265 4.05991 12.9582 4.75402 13.1729C5.44731 13.387 6.19109 13.3724 6.87545 13.1313C7.56085 12.8897 8.15062 12.4345 8.55803 11.8327L9.05774 9.21758L8.39131 7.93829L5.37845 6.56215L2.42774 9.23986L2.42817 9.24029ZM4.46602 4.51572L2.40888 4.02843L2.40974 4.02758C2.29072 3.69713 2.28112 3.33716 2.38237 3.00084C2.48362 2.66452 2.69035 2.36968 2.97202 2.15986C3.25372 1.9511 3.59548 1.83922 3.94609 1.841C4.29671 1.84278 4.63732 1.95811 4.91688 2.16972L4.46602 4.51572ZM2.2306 4.52C1.78544 4.66747 1.3969 4.94924 1.11845 5.32658C0.839302 5.70454 0.683648 6.15948 0.672756 6.62923C0.661864 7.09898 0.796264 7.56065 1.0576 7.95115C1.3186 8.34115 1.6936 8.64115 2.13117 8.80829L5.01674 6.19358L4.48745 5.05786L2.2306 4.52ZM10.0675 12.1691C9.71278 12.1685 9.36834 12.0502 9.08817 11.8327L9.5326 9.49529L11.5897 9.97743C11.6796 10.2236 11.7089 10.4877 11.6753 10.7476C11.6417 11.0075 11.546 11.2554 11.3965 11.4706C11.2477 11.6855 11.0491 11.8613 10.8177 11.983C10.5863 12.1046 10.3289 12.1685 10.0675 12.1691ZM9.50603 8.95529L11.7689 9.48586C12.2209 9.33292 12.6138 9.04253 12.8926 8.65529C13.172 8.26743 13.3238 7.80218 13.3267 7.32415C13.3266 6.86346 13.1868 6.41366 12.9256 6.03415C12.6649 5.65512 12.2949 5.36451 11.8649 5.201L8.90602 7.80243L9.50603 8.95529Z" fill="white"/>
<path id="Vector" d="M5.53465 6.05061L8.53551 7.42676L11.5638 4.76404C11.6077 4.54434 11.6295 4.3208 11.6289 4.09676C11.6292 3.37162 11.398 2.66532 10.9689 2.08076C10.5409 1.49724 9.93705 1.0663 9.24608 0.851186C8.55576 0.636644 7.81478 0.649564 7.13236 0.888043C6.44899 1.12696 5.86018 1.57862 5.45236 2.17676L4.94922 4.79876L5.53508 6.05019L5.53465 6.05061Z" fill="#FEC514"/>
<path id="Vector_2" d="M2.42774 9.24064C2.33289 9.70739 2.33678 10.1888 2.43915 10.654C2.54153 11.1191 2.74012 11.5577 3.02217 11.9415C3.45257 12.5268 4.05948 12.9586 4.75359 13.1732C5.44688 13.3873 6.19066 13.3728 6.87502 13.1316C7.56042 12.89 8.15019 12.4349 8.55759 11.8331L9.05731 9.21793L8.39088 7.93864L5.37802 6.5625L2.42731 9.24021L2.42774 9.24064Z" fill="#00BFB3"/>
<path id="Vector_3" d="M2.40851 4.02925L4.46565 4.51654L4.91651 2.17054C4.63695 1.95893 4.29634 1.84359 3.94572 1.84182C3.59511 1.84004 3.25335 1.95191 2.97165 2.16068C2.68998 2.3705 2.48325 2.66534 2.382 3.00166C2.28075 3.33798 2.29034 3.69795 2.40937 4.02839" fill="#F04E98"/>
<path id="Vector_4" d="M2.23033 4.51953C1.78517 4.66699 1.39664 4.94877 1.11819 5.3261C0.839035 5.70407 0.683381 6.15901 0.672488 6.62876C0.661596 7.09851 0.795996 7.56017 1.05733 7.95067C1.31833 8.34067 1.69333 8.64067 2.1309 8.80782L5.01647 6.1931L4.48719 5.05739L2.23033 4.51953Z" fill="#1BA9F5"/>
<path id="Vector_5" d="M9.08789 11.8335C9.36806 12.051 9.7125 12.1693 10.0672 12.17C10.3286 12.1693 10.586 12.1054 10.8174 11.9838C11.0488 11.8621 11.2474 11.6863 11.3962 11.4714C11.5457 11.2562 11.6414 11.0083 11.675 10.7484C11.7087 10.4885 11.6793 10.2244 11.5895 9.97824L9.53232 9.49609L9.08789 11.8335Z" fill="#93C90E"/>
<path id="Vector_6" d="M9.50625 8.95546L11.7691 9.48603C12.2211 9.33309 12.614 9.04269 12.8928 8.65546C13.1723 8.2676 13.324 7.80235 13.327 7.32431C13.3269 6.86363 13.187 6.41382 12.9258 6.03431C12.6651 5.65529 12.2951 5.36468 11.8651 5.20117L8.90625 7.8026L9.50625 8.95546Z" fill="#0077CC"/>
</g>
<defs>
<clipPath id="clip0_3517_16106">
<rect width="13.7143" height="13.7143" fill="white" transform="translate(0.142578 0.142578)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

View file

@ -0,0 +1,6 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.3054 10.1595H21.2814L24.063 7.35127L24.1996 6.15898C22.6062 4.73904 20.6804 3.75265 18.6044 3.293C16.5283 2.83335 14.3704 2.91561 12.3346 3.53202C10.2988 4.14842 8.45224 5.27861 6.96936 6.81583C5.48648 8.35306 4.41624 10.2466 3.85974 12.3175C4.16962 12.1892 4.5129 12.1684 4.83574 12.2584L10.399 11.3321C10.399 11.3321 10.682 10.8591 10.8284 10.8887C12.0204 9.56701 13.6678 8.7553 15.4332 8.61975C17.1986 8.48419 18.9486 9.03505 20.3249 10.1595H20.3054Z" fill="#EA4335"/>
<path d="M28.0256 12.3166C27.3863 9.93956 26.0735 7.80262 24.2485 6.16797L20.3445 10.1094C21.1581 10.7806 21.8102 11.6296 22.2514 12.5922C22.6927 13.5549 22.9117 14.6062 22.8919 15.6668V16.3664C23.3481 16.3664 23.8 16.4572 24.2215 16.6334C24.6431 16.8097 25.0261 17.0681 25.3487 17.3939C25.6714 17.7196 25.9273 18.1063 26.1019 18.5319C26.2766 18.9575 26.3664 19.4136 26.3664 19.8743C26.3664 20.335 26.2766 20.7911 26.1019 21.2167C25.9273 21.6423 25.6714 22.029 25.3487 22.3547C25.0261 22.6805 24.6431 22.9389 24.2215 23.1152C23.8 23.2914 23.3481 23.3822 22.8919 23.3822H15.9427L15.2498 24.0916V28.2991L15.9427 28.9987H22.8919C24.8324 29.014 26.7262 28.3982 28.2933 27.2426C29.8603 26.0869 31.0173 24.4528 31.593 22.5818C32.1688 20.7108 32.1328 18.7025 31.4903 16.8538C30.8479 15.0051 29.6331 13.4142 28.0256 12.3166Z" fill="#4285F4"/>
<path d="M8.98381 28.9612H15.9329V23.3446H8.98381C8.48871 23.3445 7.99942 23.237 7.54908 23.0293L6.57308 23.3348L3.77195 26.143L3.52795 27.1284C5.09876 28.3259 7.01548 28.9698 8.98381 28.9612Z" fill="#34A853"/>
<path d="M8.9838 10.7403C7.10091 10.7516 5.26856 11.3564 3.74266 12.4702C2.21676 13.5839 1.07351 15.151 0.472601 16.9526C-0.128309 18.7542 -0.156875 20.7002 0.390891 22.519C0.938658 24.3377 2.0354 25.9383 3.52794 27.0972L7.55883 23.0277C7.04659 22.7941 6.59814 22.4384 6.25197 21.9913C5.90579 21.5443 5.67222 21.019 5.5713 20.4608C5.47038 19.9025 5.50511 19.3279 5.67253 18.7861C5.83995 18.2444 6.13505 17.7518 6.53251 17.3505C6.92997 16.9492 7.41793 16.6513 7.95451 16.4823C8.4911 16.3133 9.06029 16.2782 9.61325 16.3801C10.1662 16.482 10.6864 16.7178 11.1293 17.0673C11.5721 17.4168 11.9244 17.8695 12.1558 18.3867L16.1867 14.3171C15.3406 13.2005 14.2502 12.2966 13.0007 11.6761C11.7511 11.0556 10.3763 10.7353 8.9838 10.7403Z" fill="#FBBC05"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 35 KiB

View file

@ -0,0 +1,34 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.2415 18.6003H7.03916L7.0416 13.4005H12.2441L12.2415 18.6003Z" fill="black"/>
<path d="M17.4412 23.8001H12.2389L12.2415 18.6003L17.4438 18.6004L17.4412 23.8001Z" fill="black"/>
<path d="M17.4438 18.6004L12.2415 18.6003L12.2441 13.4005H17.4464L17.4438 18.6004Z" fill="black"/>
<path d="M22.6461 18.6003L17.4438 18.6004L17.4464 13.4005H22.6487L22.6461 18.6003Z" fill="black"/>
<path d="M12.2441 13.4005H7.0416L7.04408 8.20071H12.2466L12.2441 13.4005Z" fill="black"/>
<path d="M22.6487 13.4005H17.4464L17.4488 8.20071H22.6513L22.6487 13.4005Z" fill="black"/>
<path d="M7.03916 18.6003H1.83674L1.83918 13.4005H7.0416L7.03916 18.6003Z" fill="black"/>
<path d="M7.0416 13.4005H1.83918L1.84176 8.20071H7.04408L7.0416 13.4005Z" fill="black"/>
<path d="M7.04408 8.20071H1.84176L1.84435 3.00088H7.04684L7.04408 8.20071Z" fill="black"/>
<path d="M27.8536 8.20061L22.6513 8.20071L22.6537 3.00088H27.8562L27.8536 8.20061Z" fill="black"/>
<path d="M7.0366 23.8001H1.83425L1.83674 18.6003H7.03916L7.0366 23.8001Z" fill="black"/>
<path d="M7.03401 28.9999H1.83167L1.83425 23.8001H7.0366L7.03401 28.9999Z" fill="black"/>
<path d="M27.8485 18.6003H22.6461L22.6487 13.4005H27.851L27.8485 18.6003Z" fill="black"/>
<path d="M27.851 13.4005H22.6487L22.6513 8.20071L27.8536 8.20061L27.851 13.4005Z" fill="black"/>
<path d="M27.846 23.8001H22.6436L22.6461 18.6003H27.8485L27.846 23.8001Z" fill="black"/>
<path d="M27.8434 28.9999H22.641L22.6436 23.8001H27.846L27.8434 28.9999Z" fill="black"/>
<path d="M14.405 18.5994H9.20275L9.20519 13.3997H14.4077L14.405 18.5994Z" fill="#FF7000"/>
<path d="M19.6049 23.7992H14.4026L14.405 18.5994L19.6075 18.5995L19.6049 23.7992Z" fill="#FF4900"/>
<path d="M19.6075 18.5995L14.405 18.5994L14.4077 13.3997L19.61 13.4007L19.6075 18.5995Z" fill="#FF7000"/>
<path d="M24.8099 18.6004L19.6075 18.5995L19.61 13.4007H24.8125L24.8099 18.6004Z" fill="#FF7000"/>
<path d="M14.4077 13.3997H9.20519L9.20783 8.1992H14.4103L14.4077 13.3997Z" fill="#FFA300"/>
<path d="M24.8124 13.3989H19.61L19.6125 8.1992H24.815L24.8124 13.3989Z" fill="#FFA300"/>
<path d="M9.20275 18.5994H4.00043L4.00286 13.3997H9.20519L9.20275 18.5994Z" fill="#FF7000"/>
<path d="M9.20519 13.3997H4.00286L4.00535 8.19983L9.20783 8.1992L9.20519 13.3997Z" fill="#FFA300"/>
<path d="M9.20783 8.1992L4.00535 8.19983L4.00794 3H9.21043L9.20783 8.1992Z" fill="#FFCE00"/>
<path d="M30.0172 8.19973L24.815 8.1992L24.8173 3H30.0198L30.0172 8.19973Z" fill="#FFCE00"/>
<path d="M9.20005 23.7992H3.99784L4.00043 18.5994H9.20275L9.20005 23.7992Z" fill="#FF4900"/>
<path d="M9.19756 28.9991H3.99535L3.99784 23.7992H9.20005L9.19756 28.9991Z" fill="#FF0107"/>
<path d="M30.0122 18.5994L24.8099 18.6004L24.8124 13.3989L30.0147 13.3997L30.0122 18.5994Z" fill="#FF7000"/>
<path d="M30.0147 13.3997L24.8124 13.3989L24.815 8.1992L30.0172 8.19973L30.0147 13.3997Z" fill="#FFA300"/>
<path d="M30.0096 23.7992H24.8072L24.8099 18.6004L30.0122 18.5994L30.0096 23.7992Z" fill="#FF4900"/>
<path d="M30.0071 28.9991H24.8047L24.8072 23.7992H30.0096L30.0071 28.9991Z" fill="#FF0107"/>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M28.1537 13.4601C28.4721 12.5178 28.5826 11.5176 28.4776 10.5285C28.3727 9.53937 28.0547 8.58469 27.5457 7.73014C26.7865 6.43008 25.6346 5.40425 24.2557 4.80014C22.8693 4.19126 21.327 4.03247 19.8457 4.34614C19.175 3.60107 18.3535 3.00725 17.4357 2.60414C16.5153 2.19969 15.52 1.99388 14.5147 2.00014C13.0027 1.99593 11.5276 2.46676 10.2977 3.34614C9.07577 4.21901 8.16395 5.4587 7.69468 6.88514C6.70973 7.08379 5.77797 7.48866 4.96068 8.07314C4.14712 8.65428 3.46726 9.40268 2.96668 10.2681C2.20588 11.5589 1.88068 13.0602 2.03916 14.5501C2.19764 16.04 2.83138 17.4393 3.84668 18.5411C3.52841 19.4833 3.41787 20.4832 3.52265 21.4721C3.62743 22.4611 3.94505 23.4156 4.45368 24.2701C5.21284 25.5702 6.36472 26.596 7.74368 27.2001C9.13 27.809 10.6724 27.9678 12.1537 27.6541C12.824 28.3992 13.6452 28.993 14.5627 29.3961C15.4827 29.8001 16.4787 30.0061 17.4857 30.0001C18.9986 30.0053 20.4748 29.5348 21.7057 28.6551C22.9291 27.7816 23.8417 26.5404 24.3107 25.1121C25.2956 24.9139 26.2274 24.5094 27.0447 23.9251C27.858 23.3436 28.5376 22.5949 29.0377 21.7291C29.7974 20.4383 30.1216 18.9373 29.9624 17.448C29.8032 15.9587 29.1691 14.5602 28.1537 13.4591V13.4601ZM17.5437 28.1701C16.1317 28.1701 15.0387 27.7421 14.0837 26.9551C14.1267 26.9321 14.2027 26.8911 14.2517 26.8611L19.9017 23.6411C20.0423 23.5619 20.1594 23.4468 20.241 23.3074C20.3225 23.1681 20.3655 23.0096 20.3657 22.8481V14.9881L22.7547 16.3481C22.767 16.3545 22.7776 16.3638 22.7856 16.3751C22.7936 16.3864 22.7987 16.3994 22.8007 16.4131V22.9211C22.8007 25.8731 20.3097 28.1701 17.5437 28.1701ZM6.06268 23.3541C5.43989 22.2948 5.21478 21.0484 5.42768 19.8381C5.46968 19.8631 5.54268 19.9081 5.59568 19.9381L11.2457 23.1581C11.3867 23.2396 11.5468 23.2825 11.7097 23.2825C11.8726 23.2825 12.0326 23.2396 12.1737 23.1581L19.0717 19.2281V21.9481C19.0725 21.962 19.0698 21.9759 19.0639 21.9885C19.0579 22.0011 19.0489 22.012 19.0377 22.0201L13.3267 25.2751C12.1017 25.97 10.6532 26.1574 9.29168 25.7971C7.9348 25.441 6.77475 24.5628 6.06268 23.3541ZM4.57368 11.1841C5.19855 10.1168 6.17811 9.30248 7.34168 8.88314V15.5151C7.34012 15.6769 7.38245 15.8361 7.46415 15.9757C7.54586 16.1154 7.66388 16.2302 7.80568 16.3081L14.7027 20.2381L12.3147 21.5981C12.303 21.6058 12.2896 21.6105 12.2757 21.6119C12.2617 21.6133 12.2477 21.6113 12.2347 21.6061L6.52068 18.3491C5.91893 18.0071 5.39058 17.5497 4.96588 17.0032C4.54118 16.4567 4.22847 15.8317 4.04568 15.1641C3.86493 14.499 3.81844 13.8045 3.90892 13.1211C3.9994 12.4378 4.22604 11.7793 4.57368 11.1841ZM24.1967 15.6901L17.2987 11.7601L19.6867 10.4001C19.6984 10.3925 19.7118 10.3877 19.7257 10.3863C19.7396 10.385 19.7537 10.3869 19.7667 10.3921L25.4797 13.6471C26.3517 14.1436 27.0642 14.8783 27.5337 15.7651C28.0004 16.6478 28.2026 17.6464 28.116 18.641C28.0295 19.6357 27.6578 20.5844 27.0457 21.3731C26.4299 22.1683 25.6011 22.7723 24.6557 23.1151V16.4821C24.6574 16.3211 24.6157 16.1625 24.535 16.0231C24.4542 15.8837 24.3373 15.7687 24.1967 15.6901ZM26.5737 12.1571C26.5181 12.1235 26.4621 12.0905 26.4057 12.0581L20.7557 8.83814C20.6145 8.75688 20.4545 8.71412 20.2917 8.71412C20.1288 8.71412 19.9688 8.75688 19.8277 8.83814L12.9297 12.7681V10.0461C12.9289 10.0322 12.9315 10.0184 12.9375 10.0058C12.9434 9.99321 12.9524 9.98233 12.9637 9.97414L18.6757 6.72314C19.5524 6.22471 20.5517 5.98299 21.5593 6.02561C22.5669 6.06823 23.5422 6.39346 24.3737 6.96414C25.1997 7.53109 25.8467 8.32209 26.2387 9.24414C26.6287 10.1641 26.7447 11.1741 26.5737 12.1571ZM11.6317 17.0091L9.24168 15.6491C9.22917 15.643 9.21841 15.6339 9.21037 15.6225C9.20233 15.6112 9.19728 15.598 9.19568 15.5841V9.07514C9.19668 8.07814 9.48568 7.10214 10.0277 6.26114C10.5729 5.41768 11.3466 4.7466 12.2587 4.32614C13.1746 3.90382 14.1908 3.74739 15.1914 3.8747C16.192 4.002 17.1366 4.40791 17.9177 5.04614C17.8611 5.07604 17.8051 5.10704 17.7497 5.13914L12.0997 8.35914C11.959 8.43838 11.8418 8.55353 11.7601 8.69284C11.6784 8.83216 11.6351 8.99064 11.6347 9.15214L11.6317 17.0091ZM12.9287 14.2491L16.0007 12.5001L19.0727 14.2501V17.7501L16.0007 19.4991L12.9287 17.7491V14.2491Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View file

@ -8,8 +8,9 @@
import {
SortFieldInferenceEndpoint,
QueryParams,
AlInferenceEndpointsTableState,
AllInferenceEndpointsTableState,
SortOrder,
FilterOptions,
} from './types';
export const DEFAULT_TABLE_ACTIVE_PAGE = 1;
@ -22,6 +23,12 @@ export const DEFAULT_QUERY_PARAMS: QueryParams = {
sortOrder: SortOrder.asc,
};
export const DEFAULT_INFERENCE_ENDPOINTS_TABLE_STATE: AlInferenceEndpointsTableState = {
export const DEFAULT_FILTER_OPTIONS: FilterOptions = {
provider: [],
type: [],
};
export const DEFAULT_INFERENCE_ENDPOINTS_TABLE_STATE: AllInferenceEndpointsTableState = {
filterOptions: DEFAULT_FILTER_OPTIONS,
queryParams: DEFAULT_QUERY_PARAMS,
};

View file

@ -0,0 +1,66 @@
/*
* 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 { render, fireEvent, waitFor } from '@testing-library/react';
import { MultiSelectFilter, MultiSelectFilterOption } from './multi_select_filter';
import '@testing-library/jest-dom/extend-expect';
describe('MultiSelectFilter', () => {
const options: MultiSelectFilterOption[] = [
{ key: '1', label: 'Option 1', checked: 'off' },
{ key: '2', label: 'Option 2', checked: 'on' },
{ key: '3', label: 'Option 3', checked: 'off' },
];
it('should render the filter button with the provided label', () => {
const { getByText } = render(
<MultiSelectFilter onChange={() => {}} options={options} buttonLabel="Filter Options" />
);
expect(getByText('Filter Options')).toBeInTheDocument();
});
it('should toggle the popover when the filter button is clicked', async () => {
const { getByText, queryByText } = render(
<MultiSelectFilter onChange={() => {}} options={options} buttonLabel="Filter Options" />
);
fireEvent.click(getByText('Filter Options'));
expect(queryByText('Option 1')).toBeInTheDocument();
fireEvent.click(getByText('Filter Options'));
await waitFor(() => {
expect(queryByText('Option 1')).not.toBeInTheDocument();
});
});
it('should render the provided options', async () => {
const { getByText } = render(
<MultiSelectFilter onChange={() => {}} options={options} buttonLabel="Filter Options" />
);
fireEvent.click(getByText('Filter Options'));
await waitFor(() => {
expect(getByText('Option 1')).toBeInTheDocument();
expect(getByText('Option 2')).toBeInTheDocument();
expect(getByText('Option 3')).toBeInTheDocument();
});
});
it('should call the onChange function with the updated options when an option is clicked', async () => {
const onChange = jest.fn();
const { getByText } = render(
<MultiSelectFilter onChange={onChange} options={options} buttonLabel="Filter Options" />
);
fireEvent.click(getByText('Filter Options'));
fireEvent.click(getByText('Option 1'));
await waitFor(() => {
expect(onChange).toHaveBeenCalled();
});
});
});

View file

@ -0,0 +1,108 @@
/*
* 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 {
EuiFilterButton,
EuiFilterGroup,
EuiPopover,
EuiPopoverTitle,
EuiSelectable,
EuiSpacer,
EuiText,
EuiTextColor,
useEuiTheme,
} from '@elastic/eui';
import { css } from '@emotion/react';
import React, { useState } from 'react';
import * as i18n from './translations';
export interface MultiSelectFilterOption {
key: string;
label: string;
checked?: 'on' | 'off';
}
interface UseFilterParams {
buttonLabel?: string;
onChange: (newOptions: MultiSelectFilterOption[]) => void;
options: MultiSelectFilterOption[];
renderOption?: (option: MultiSelectFilterOption) => React.ReactNode;
selectedOptionKeys?: string[];
}
export const MultiSelectFilter: React.FC<UseFilterParams> = ({
buttonLabel,
onChange,
options: rawOptions,
selectedOptionKeys = [],
renderOption,
}) => {
const { euiTheme } = useEuiTheme();
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const toggleIsPopoverOpen = () => setIsPopoverOpen((prevValue) => !prevValue);
const options: MultiSelectFilterOption[] = rawOptions.map(({ key, label }) => ({
label,
key,
checked: selectedOptionKeys.includes(key) ? 'on' : undefined,
}));
return (
<EuiFilterGroup>
<EuiPopover
ownFocus
button={
<EuiFilterButton
iconType={'arrowDown'}
onClick={toggleIsPopoverOpen}
isSelected={isPopoverOpen}
numFilters={options.length}
hasActiveFilters={selectedOptionKeys.length > 0}
numActiveFilters={selectedOptionKeys.length}
aria-label={buttonLabel}
>
<EuiText size="s" className="eui-textTruncate">
{buttonLabel}
</EuiText>
</EuiFilterButton>
}
isOpen={isPopoverOpen}
closePopover={() => setIsPopoverOpen(false)}
panelPaddingSize="none"
repositionOnScroll
>
<EuiSelectable
options={options}
searchable
searchProps={{
placeholder: buttonLabel,
}}
emptyMessage="No options"
onChange={onChange}
singleSelection={false}
renderOption={renderOption}
>
{(list, search) => (
<div>
<EuiPopoverTitle paddingSize="s">{search}</EuiPopoverTitle>
<div
css={css`
line-height: ${euiTheme.size.xl};
padding-left: ${euiTheme.size.m};
border-bottom: ${euiTheme.border.thin};
`}
>
<EuiTextColor color="subdued">{i18n.OPTIONS(options.length)}</EuiTextColor>
</div>
<EuiSpacer size="xs" />
{list}
</div>
)}
</EuiSelectable>
</EuiPopover>
</EuiFilterGroup>
);
};

View file

@ -0,0 +1,43 @@
/*
* 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 { SERVICE_PROVIDERS } from '../render_table_columns/render_service_provider/service_provider';
import type { FilterOptions, ServiceProviderKeys } from '../types';
import { MultiSelectFilter, MultiSelectFilterOption } from './multi_select_filter';
import * as i18n from './translations';
interface Props {
optionKeys: ServiceProviderKeys[];
onChange: (newFilterOptions: Partial<FilterOptions>) => void;
}
const options = Object.entries(SERVICE_PROVIDERS).map(([key, { name }]) => ({
key,
label: name,
}));
export const ServiceProviderFilter: React.FC<Props> = ({ optionKeys, onChange }) => {
const filterId: string = 'provider';
const onSystemFilterChange = (newOptions: MultiSelectFilterOption[]) => {
onChange({
[filterId]: newOptions
.filter((option) => option.checked === 'on')
.map((option) => option.key),
});
};
return (
<MultiSelectFilter
buttonLabel={i18n.SERVICE_PROVIDER}
onChange={onSystemFilterChange}
options={options}
renderOption={(option) => option.label}
selectedOptionKeys={optionKeys}
/>
);
};

View file

@ -0,0 +1,42 @@
/*
* 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 { FilterOptions, TaskTypes } from '../types';
import { MultiSelectFilter, MultiSelectFilterOption } from './multi_select_filter';
import * as i18n from './translations';
interface Props {
optionKeys: TaskTypes[];
onChange: (newFilterOptions: Partial<FilterOptions>) => void;
}
const options = Object.values(TaskTypes).map((option) => ({
key: option,
label: option,
}));
export const TaskTypeFilter: React.FC<Props> = ({ optionKeys, onChange }) => {
const filterId: string = 'type';
const onSystemFilterChange = (newOptions: MultiSelectFilterOption[]) => {
onChange({
[filterId]: newOptions
.filter((option) => option.checked === 'on')
.map((option) => option.key),
});
};
return (
<MultiSelectFilter
buttonLabel={i18n.TASK_TYPE}
onChange={onSystemFilterChange}
options={options}
renderOption={(option) => option.label}
selectedOptionKeys={optionKeys}
/>
);
};

View file

@ -0,0 +1,22 @@
/*
* 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 { i18n } from '@kbn/i18n';
export { SERVICE_PROVIDER, TASK_TYPE } from '../../../../common/translations';
export const EMPTY_FILTER_MESSAGE = i18n.translate(
'xpack.searchInferenceEndpoints.filter.emptyMessage',
{
defaultMessage: 'No options',
}
);
export const OPTIONS = (totalCount: number) =>
i18n.translate('xpack.searchInferenceEndpoints.filter.options', {
defaultMessage: '{totalCount, plural, one {# option} other {# options}}',
values: { totalCount },
});

View file

@ -0,0 +1,73 @@
/*
* 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 { renderReactTestingLibraryWithI18n as render } from '@kbn/test-jest-helpers';
import React from 'react';
import { useKibana } from '../../../../../../hooks/use_kibana';
import { useCopyIDAction } from './use_copy_id_action';
const mockInferenceEndpoint = {
deployment: 'not_applicable',
endpoint: {
model_id: 'hugging-face-embeddings',
task_type: 'text_embedding',
service: 'hugging_face',
service_settings: {
dimensions: 768,
rate_limit: {
requests_per_minute: 3000,
},
},
task_settings: {},
},
provider: 'hugging_face',
type: 'text_embedding',
} as any;
Object.defineProperty(navigator, 'clipboard', {
value: {
writeText: jest.fn().mockResolvedValue(undefined),
},
configurable: true,
});
const mockOnActionSuccess = jest.fn();
jest.mock('../../../../../../hooks/use_kibana', () => ({
useKibana: jest.fn(),
}));
const addSuccess = jest.fn();
(useKibana as jest.Mock).mockImplementation(() => ({
services: {
notifications: {
toasts: {
addSuccess,
},
},
},
}));
describe('useCopyIDAction hook', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('renders the label with correct text', () => {
const TestComponent = () => {
const { getAction } = useCopyIDAction({ onActionSuccess: mockOnActionSuccess });
const action = getAction(mockInferenceEndpoint);
return <div>{action}</div>;
};
const { getByTestId } = render(<TestComponent />);
const labelElement = getByTestId('inference-endpoints-action-copy-id-label');
expect(labelElement).toHaveTextContent('Copy endpoint ID');
});
});

View file

@ -0,0 +1,44 @@
/*
* 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 { EuiContextMenuItem, EuiCopy, EuiIcon } from '@elastic/eui';
import React from 'react';
import * as i18n from '../../../../../../../common/translations';
import { useKibana } from '../../../../../../hooks/use_kibana';
import { InferenceEndpointUI } from '../../../../types';
import { UseCopyIDActionProps } from '../types';
export const useCopyIDAction = ({ onActionSuccess }: UseCopyIDActionProps) => {
const {
services: { notifications },
} = useKibana();
const toasts = notifications?.toasts;
const getAction = (inferenceEndpoint: InferenceEndpointUI) => {
return (
<EuiCopy textToCopy={inferenceEndpoint.endpoint.model_id} anchorClassName="eui-fullWidth">
{(copy) => (
<EuiContextMenuItem
key="copy"
data-test-subj="inference-endpoints-action-copy-id-label"
icon={<EuiIcon type="copyClipboard" size="m" />}
onClick={() => {
copy();
onActionSuccess();
toasts?.addSuccess({ title: i18n.COPY_ID_ACTION_SUCCESS });
}}
size="s"
>
{i18n.COPY_ID_ACTION_LABEL}
</EuiContextMenuItem>
)}
</EuiCopy>
);
};
return { getAction };
};

View file

@ -0,0 +1,41 @@
/*
* 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 { render, fireEvent, screen } from '@testing-library/react';
import React from 'react';
import { ConfirmDeleteEndpointModal } from '.';
import * as i18n from './translations';
describe('ConfirmDeleteEndpointModal', () => {
const mockOnCancel = jest.fn();
const mockOnConfirm = jest.fn();
beforeEach(() => {
render(<ConfirmDeleteEndpointModal onCancel={mockOnCancel} onConfirm={mockOnConfirm} />);
});
it('renders the modal with correct texts', () => {
expect(screen.getByText(i18n.DELETE_TITLE)).toBeInTheDocument();
expect(screen.getByText(i18n.CONFIRM_DELETE_WARNING)).toBeInTheDocument();
expect(screen.getByText(i18n.CANCEL)).toBeInTheDocument();
expect(screen.getByText(i18n.DELETE_ACTION_LABEL)).toBeInTheDocument();
});
it('calls onCancel when the cancel button is clicked', () => {
fireEvent.click(screen.getByText(i18n.CANCEL));
expect(mockOnCancel).toHaveBeenCalled();
});
it('calls onConfirm when the delete button is clicked', () => {
fireEvent.click(screen.getByText(i18n.DELETE_ACTION_LABEL));
expect(mockOnConfirm).toHaveBeenCalled();
});
it('has the delete button focused by default', () => {
expect(document.activeElement).toHaveTextContent(i18n.DELETE_ACTION_LABEL);
});
});

View file

@ -0,0 +1,34 @@
/*
* 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 { EuiConfirmModal } from '@elastic/eui';
import * as i18n from './translations';
interface ConfirmDeleteEndpointModalProps {
onCancel: () => void;
onConfirm: () => void;
}
export const ConfirmDeleteEndpointModal: React.FC<ConfirmDeleteEndpointModalProps> = ({
onCancel,
onConfirm,
}) => {
return (
<EuiConfirmModal
buttonColor="danger"
cancelButtonText={i18n.CANCEL}
confirmButtonText={i18n.DELETE_ACTION_LABEL}
defaultFocusedButton="confirm"
onCancel={onCancel}
onConfirm={onConfirm}
title={i18n.DELETE_TITLE}
>
{i18n.CONFIRM_DELETE_WARNING}
</EuiConfirmModal>
);
};

View file

@ -0,0 +1,24 @@
/*
* 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 { i18n } from '@kbn/i18n';
export * from '../../../../../../../../common/translations';
export const DELETE_TITLE = i18n.translate(
'xpack.searchInferenceEndpoints.confirmDeleteEndpoint.title',
{
defaultMessage: 'Delete inference endpoint',
}
);
export const CONFIRM_DELETE_WARNING = i18n.translate(
'xpack.searchInferenceEndpoints.confirmDeleteEndpoint.confirmQuestion',
{
defaultMessage:
'Deleting an active endpoint will cause operations targeting associated semantic_text fields and inference pipelines to fail.',
}
);

View file

@ -0,0 +1,55 @@
/*
* 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 { EuiContextMenuItem, EuiIcon } from '@elastic/eui';
import React, { useCallback, useState } from 'react';
import * as i18n from '../../../../../../../common/translations';
import { useDeleteEndpoint } from '../../../../../../hooks/use_delete_endpoint';
import { InferenceEndpointUI } from '../../../../types';
import type { UseActionProps } from '../types';
export const useDeleteAction = ({ onActionSuccess }: UseActionProps) => {
const [isModalVisible, setIsModalVisible] = useState<boolean>(false);
const [endpointToBeDeleted, setEndpointToBeDeleted] = useState<InferenceEndpointUI | null>(null);
const onCloseModal = useCallback(() => setIsModalVisible(false), []);
const openModal = useCallback(
(selectedEndpoint: InferenceEndpointUI) => {
onActionSuccess();
setIsModalVisible(true);
setEndpointToBeDeleted(selectedEndpoint);
},
[onActionSuccess]
);
const { mutate: deleteEndpoint } = useDeleteEndpoint();
const onConfirmDeletion = useCallback(() => {
onCloseModal();
if (!endpointToBeDeleted) {
return;
}
deleteEndpoint({
type: endpointToBeDeleted.type,
id: endpointToBeDeleted.endpoint.model_id,
});
}, [deleteEndpoint, onCloseModal, endpointToBeDeleted]);
const getAction = (selectedEndpoint: InferenceEndpointUI) => {
return (
<EuiContextMenuItem
key="delete"
icon={<EuiIcon type="trash" size="m" color={'danger'} />}
onClick={() => openModal(selectedEndpoint)}
>
{i18n.DELETE_ACTION_LABEL}
</EuiContextMenuItem>
);
};
return { getAction, isModalVisible, onConfirmDeletion, onCloseModal };
};

View file

@ -0,0 +1,12 @@
/*
* 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.
*/
export interface UseActionProps {
onActionSuccess: () => void;
}
export type UseCopyIDActionProps = Pick<UseActionProps, 'onActionSuccess'>;

View file

@ -0,0 +1,79 @@
/*
* 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 type { EuiTableComputedColumnType } from '@elastic/eui';
import { EuiButtonIcon, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
import React, { useCallback, useState } from 'react';
import { InferenceEndpointUI } from '../../types';
import { useCopyIDAction } from './actions/copy_id/use_copy_id_action';
import { ConfirmDeleteEndpointModal } from './actions/delete/confirm_delete_endpoint';
import { useDeleteAction } from './actions/delete/use_delete_action';
export const ActionColumn: React.FC<{ interfaceEndpoint: InferenceEndpointUI }> = ({
interfaceEndpoint,
}) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const tooglePopover = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [isPopoverOpen]);
const closePopover = useCallback(() => setIsPopoverOpen(false), []);
const copyIDAction = useCopyIDAction({
onActionSuccess: closePopover,
});
const deleteAction = useDeleteAction({
onActionSuccess: closePopover,
});
const items = [
copyIDAction.getAction(interfaceEndpoint),
deleteAction.getAction(interfaceEndpoint),
];
return (
<>
<EuiPopover
button={
<EuiButtonIcon
onClick={tooglePopover}
iconType="boxesHorizontal"
aria-label={'Actions'}
color="text"
disabled={false}
/>
}
isOpen={isPopoverOpen}
closePopover={closePopover}
panelPaddingSize="none"
anchorPosition="downLeft"
>
<EuiContextMenuPanel size="s" items={items} />
</EuiPopover>
{deleteAction.isModalVisible ? (
<ConfirmDeleteEndpointModal
onCancel={deleteAction.onCloseModal}
onConfirm={deleteAction.onConfirmDeletion}
/>
) : null}
</>
);
};
interface UseBulkActionsReturnValue {
actions: EuiTableComputedColumnType<InferenceEndpointUI>;
}
export const useActions = (): UseBulkActionsReturnValue => {
return {
actions: {
align: 'right',
render: (interfaceEndpoint: InferenceEndpointUI) => {
return <ActionColumn interfaceEndpoint={interfaceEndpoint} />;
},
width: '165px',
},
};
};

View file

@ -0,0 +1,27 @@
/*
* 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 { render, screen } from '@testing-library/react';
import React from 'react';
import { DeploymentStatus } from './deployment_status';
import { DeploymentStatusEnum } from '../../types';
describe('DeploymentStatus component', () => {
it.each([[DeploymentStatusEnum.deployed, DeploymentStatusEnum.notDeployed]])(
'renders with %s status, expects %s color, and correct data-test-subj attribute',
(status) => {
render(<DeploymentStatus status={status} />);
const healthComponent = screen.getByTestId(`table-column-deployment-${status}`);
expect(healthComponent).toBeInTheDocument();
}
);
it('does not render when status is notApplicable', () => {
const { container } = render(<DeploymentStatus status={DeploymentStatusEnum.notApplicable} />);
expect(container).toBeEmptyDOMElement();
});
});

View file

@ -0,0 +1,52 @@
/*
* 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 { EuiIcon, EuiToolTip } from '@elastic/eui';
import { DeploymentStatusEnum } from '../../types';
import * as i18n from './translations';
interface DeploymentStatusProps {
status: DeploymentStatusEnum;
}
export const DeploymentStatus: React.FC<DeploymentStatusProps> = ({ status }) => {
if (status === DeploymentStatusEnum.notApplicable) {
return null;
}
let statusColor: string;
let type: string;
let tooltip: string;
switch (status) {
case DeploymentStatusEnum.deployed:
statusColor = 'success';
type = 'dot';
tooltip = i18n.MODEL_DEPLOYED;
break;
case DeploymentStatusEnum.notDeployed:
statusColor = 'warning';
type = 'warning';
tooltip = i18n.MODEL_NOT_DEPLOYED;
break;
case DeploymentStatusEnum.notDeployable:
statusColor = 'danger';
type = 'dot';
tooltip = i18n.MODEL_FAILED_TO_BE_DEPLOYED;
}
return (
<EuiToolTip content={tooltip}>
<EuiIcon
type={type}
data-test-subj={`table-column-deployment-${status}`}
color={statusColor}
/>
</EuiToolTip>
);
};

View file

@ -0,0 +1,29 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const MODEL_DEPLOYED = i18n.translate(
'xpack.searchInferenceEndpoints.deploymentStatus.tooltip.modelDeployed',
{
defaultMessage: 'Model is deployed',
}
);
export const MODEL_NOT_DEPLOYED = i18n.translate(
'xpack.searchInferenceEndpoints.deploymentStatus.tooltip.modelNotDeployed',
{
defaultMessage: 'Model is not deployed',
}
);
export const MODEL_FAILED_TO_BE_DEPLOYED = i18n.translate(
'xpack.searchInferenceEndpoints.deploymentStatus.tooltip.modelFailedToBeDeployed',
{
defaultMessage: 'Model can not be deployed',
}
);

View file

@ -0,0 +1,257 @@
/*
* 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 { render, screen } from '@testing-library/react';
import React from 'react';
import { EndpointInfo } from './endpoint_info';
describe('RenderEndpoint component tests', () => {
describe('with cohere service', () => {
const mockEndpoint = {
model_id: 'cohere-2',
service: 'cohere',
service_settings: {
similarity: 'cosine',
dimensions: 384,
model_id: 'embed-english-light-v3.0',
rate_limit: {
requests_per_minute: 10000,
},
embedding_type: 'byte',
},
task_settings: {},
} as any;
it('renders the component with endpoint details for Cohere service', () => {
render(<EndpointInfo endpoint={mockEndpoint} />);
expect(screen.getByText('cohere-2')).toBeInTheDocument();
expect(screen.getByText('byte')).toBeInTheDocument();
expect(screen.getByText('embed-english-light-v3.0')).toBeInTheDocument();
});
it('does not render model_id badge if serviceSettings.model_id is not provided for Cohere service', () => {
const modifiedEndpoint = {
...mockEndpoint,
service_settings: { ...mockEndpoint.service_settings, model_id: undefined },
};
render(<EndpointInfo endpoint={modifiedEndpoint} />);
expect(screen.queryByText('embed-english-light-v3.0')).not.toBeInTheDocument();
});
it('renders only model_id if other settings are not provided for Cohere service', () => {
const modifiedEndpoint = {
...mockEndpoint,
service_settings: { model_id: 'embed-english-light-v3.0' },
};
render(<EndpointInfo endpoint={modifiedEndpoint} />);
expect(screen.getByText('embed-english-light-v3.0')).toBeInTheDocument();
expect(screen.queryByText(',')).not.toBeInTheDocument();
});
});
describe('with elasticsearch service', () => {
const mockEndpoint = {
model_id: 'model-123',
service: 'elasticsearch',
service_settings: {
num_allocations: 5,
num_threads: 10,
model_id: 'settings-model-123',
},
} as any;
it('renders the component with endpoint model_id and model settings', () => {
render(<EndpointInfo endpoint={mockEndpoint} />);
expect(screen.getByText('model-123')).toBeInTheDocument();
expect(screen.getByText('settings-model-123')).toBeInTheDocument();
expect(screen.getByText('Threads: 10 | Allocations: 5')).toBeInTheDocument();
});
it('renders the component with only model_id if num_threads and num_allocations are not provided', () => {
const modifiedSettings = {
...mockEndpoint.service_settings,
num_threads: undefined,
num_allocations: undefined,
};
const modifiedEndpoint = { ...mockEndpoint, service_settings: modifiedSettings };
render(<EndpointInfo endpoint={modifiedEndpoint} />);
expect(screen.getByText('model-123')).toBeInTheDocument();
expect(screen.getByText('settings-model-123')).toBeInTheDocument();
expect(screen.queryByText('Threads: 10 | Allocations: 5')).not.toBeInTheDocument();
});
});
describe('with azureaistudio service', () => {
const mockEndpoint = {
model_id: 'azure-ai-1',
service: 'azureaistudio',
service_settings: {
target: 'westus',
provider: 'microsoft_phi',
endpoint_type: 'realtime',
},
} as any;
it('renders the component with endpoint details', () => {
render(<EndpointInfo endpoint={mockEndpoint} />);
expect(screen.getByText('azure-ai-1')).toBeInTheDocument();
expect(screen.getByText('microsoft_phi, realtime, westus')).toBeInTheDocument();
});
it('renders correctly when some service settings are missing', () => {
const modifiedEndpoint = {
...mockEndpoint,
service_settings: { target: 'westus', provider: 'microsoft_phi' },
};
render(<EndpointInfo endpoint={modifiedEndpoint} />);
expect(screen.getByText('microsoft_phi, westus')).toBeInTheDocument();
});
it('does not render a comma when only one service setting is provided', () => {
const modifiedEndpoint = {
...mockEndpoint,
service_settings: { target: 'westus' },
};
render(<EndpointInfo endpoint={modifiedEndpoint} />);
expect(screen.getByText('westus')).toBeInTheDocument();
expect(screen.queryByText(',')).not.toBeInTheDocument();
});
it('renders nothing related to service settings when all are missing', () => {
const modifiedEndpoint = {
...mockEndpoint,
service_settings: {},
};
render(<EndpointInfo endpoint={modifiedEndpoint} />);
expect(screen.getByText('azure-ai-1')).toBeInTheDocument();
expect(screen.queryByText('westus')).not.toBeInTheDocument();
expect(screen.queryByText('microsoft_phi')).not.toBeInTheDocument();
expect(screen.queryByText('realtime')).not.toBeInTheDocument();
});
});
describe('with azureopenai service', () => {
const mockEndpoint = {
model_id: 'azure-openai-1',
service: 'azureopenai',
service_settings: {
resource_name: 'resource-xyz',
deployment_id: 'deployment-123',
api_version: 'v1',
},
} as any;
it('renders the component with all required endpoint details', () => {
render(<EndpointInfo endpoint={mockEndpoint} />);
expect(screen.getByText('azure-openai-1')).toBeInTheDocument();
expect(screen.getByText('resource-xyz, deployment-123, v1')).toBeInTheDocument();
});
});
describe('with mistral service', () => {
const mockEndpoint = {
model_id: 'mistral-ai-1',
service: 'mistral',
service_settings: {
model: 'model-xyz',
max_input_tokens: 512,
rate_limit: {
requests_per_minute: 1000,
},
},
} as any;
it('renders the component with endpoint details', () => {
render(<EndpointInfo endpoint={mockEndpoint} />);
expect(screen.getByText('mistral-ai-1')).toBeInTheDocument();
expect(screen.getByText('model-xyz')).toBeInTheDocument();
expect(screen.getByText('max_input_tokens: 512, rate_limit: 1000')).toBeInTheDocument();
});
it('renders correctly when some service settings are missing', () => {
const modifiedEndpoint = {
...mockEndpoint,
service_settings: {
model: 'model-xyz',
max_input_tokens: 512,
},
};
render(<EndpointInfo endpoint={modifiedEndpoint} />);
expect(screen.getByText('max_input_tokens: 512')).toBeInTheDocument();
});
it('does not render a comma when only one service setting is provided', () => {
const modifiedEndpoint = {
...mockEndpoint,
service_settings: { model: 'model-xyz' },
};
render(<EndpointInfo endpoint={modifiedEndpoint} />);
expect(screen.getByText('model-xyz')).toBeInTheDocument();
expect(screen.queryByText(',')).not.toBeInTheDocument();
});
it('renders nothing related to service settings when all are missing', () => {
const modifiedEndpoint = {
...mockEndpoint,
service_settings: {},
};
render(<EndpointInfo endpoint={modifiedEndpoint} />);
expect(screen.getByText('mistral-ai-1')).toBeInTheDocument();
expect(screen.queryByText('model-xyz')).not.toBeInTheDocument();
expect(screen.queryByText('max_input_tokens: 512')).not.toBeInTheDocument();
expect(screen.queryByText('rate_limit: 1000')).not.toBeInTheDocument();
});
});
describe('with googleaistudio service', () => {
const mockEndpoint = {
model_id: 'google-ai-1',
service: 'googleaistudio',
service_settings: {
model_id: 'model-abc',
rate_limit: {
requests_per_minute: 500,
},
},
} as any;
it('renders the component with endpoint details', () => {
render(<EndpointInfo endpoint={mockEndpoint} />);
expect(screen.getByText('model-abc')).toBeInTheDocument();
expect(screen.getByText('rate_limit: 500')).toBeInTheDocument();
});
it('renders correctly when rate limit is missing', () => {
const modifiedEndpoint = {
...mockEndpoint,
service_settings: {
model_id: 'model-abc',
},
};
render(<EndpointInfo endpoint={modifiedEndpoint} />);
expect(screen.getByText('model-abc')).toBeInTheDocument();
expect(screen.queryByText('Rate limit:')).not.toBeInTheDocument();
});
});
});

View file

@ -0,0 +1,164 @@
/*
* 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 { InferenceAPIConfigResponse } from '@kbn/ml-trained-models-utils';
import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { ServiceProviderKeys } from '../../types';
import { ModelBadge } from './model_badge';
import * as i18n from './translations';
export interface EndpointInfoProps {
endpoint: InferenceAPIConfigResponse;
}
export const EndpointInfo: React.FC<EndpointInfoProps> = ({ endpoint }) => {
return (
<EuiFlexGroup gutterSize="s" direction="column">
<EuiFlexItem grow={false}>
<strong>{endpoint.model_id}</strong>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EndpointModelInfo endpoint={endpoint} />
</EuiFlexItem>
</EuiFlexGroup>
);
};
export const EndpointModelInfo: React.FC<EndpointInfoProps> = ({ endpoint }) => {
const serviceSettings = endpoint.service_settings;
const modelId =
'model_id' in serviceSettings
? serviceSettings.model_id
: 'model' in serviceSettings
? serviceSettings.model
: undefined;
return (
<EuiFlexGroup gutterSize="s" alignItems="center">
{modelId && (
<EuiFlexItem grow={false}>
<ModelBadge model={modelId} />
</EuiFlexItem>
)}
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="xs">
{endpointModelAtrributes(endpoint)}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
};
function endpointModelAtrributes(endpoint: InferenceAPIConfigResponse) {
switch (endpoint.service) {
case ServiceProviderKeys.elser:
case ServiceProviderKeys.elasticsearch:
return elasticsearchAttributes(endpoint);
case ServiceProviderKeys.cohere:
return cohereAttributes(endpoint);
case ServiceProviderKeys.hugging_face:
return huggingFaceAttributes(endpoint);
case ServiceProviderKeys.openai:
return openAIAttributes(endpoint);
case ServiceProviderKeys.azureaistudio:
return azureOpenAIStudioAttributes(endpoint);
case ServiceProviderKeys.azureopenai:
return azureOpenAIAttributes(endpoint);
case ServiceProviderKeys.mistral:
return mistralAttributes(endpoint);
case ServiceProviderKeys.googleaistudio:
return googleAIStudioAttributes(endpoint);
default:
return null;
}
}
function elasticsearchAttributes(endpoint: InferenceAPIConfigResponse) {
const serviceSettings = endpoint.service_settings;
const numAllocations =
'num_allocations' in serviceSettings ? serviceSettings.num_allocations : undefined;
const numThreads = 'num_threads' in serviceSettings ? serviceSettings.num_threads : undefined;
return `${numThreads ? i18n.THREADS(numThreads) : ''}${
numThreads && numAllocations ? ' | ' : ''
}${numAllocations ? i18n.ALLOCATIONS(numAllocations) : ''}`;
}
function cohereAttributes(endpoint: InferenceAPIConfigResponse) {
const serviceSettings = endpoint.service_settings;
const embeddingType =
'embedding_type' in serviceSettings ? serviceSettings.embedding_type : undefined;
const taskSettings = endpoint.task_settings;
const inputType = 'input_type' in taskSettings ? taskSettings.input_type : undefined;
const truncate = 'truncate' in taskSettings ? taskSettings.truncate : undefined;
return [embeddingType, inputType, truncate && `truncate: ${truncate}`].filter(Boolean).join(', ');
}
function huggingFaceAttributes(endpoint: InferenceAPIConfigResponse) {
const serviceSettings = endpoint.service_settings;
const url = 'url' in serviceSettings ? serviceSettings.url : null;
return url;
}
function openAIAttributes(endpoint: InferenceAPIConfigResponse) {
const serviceSettings = endpoint.service_settings;
const url = 'url' in serviceSettings ? serviceSettings.url : null;
return url;
}
function azureOpenAIStudioAttributes(endpoint: InferenceAPIConfigResponse) {
const serviceSettings = endpoint.service_settings;
const provider = 'provider' in serviceSettings ? serviceSettings.provider : undefined;
const endpointType =
'endpoint_type' in serviceSettings ? serviceSettings.endpoint_type : undefined;
const target = 'target' in serviceSettings ? serviceSettings.target : undefined;
return [provider, endpointType, target].filter(Boolean).join(', ');
}
function azureOpenAIAttributes(endpoint: InferenceAPIConfigResponse) {
const serviceSettings = endpoint.service_settings;
const resourceName =
'resource_name' in serviceSettings ? serviceSettings.resource_name : undefined;
const deploymentId =
'deployment_id' in serviceSettings ? serviceSettings.deployment_id : undefined;
const apiVersion = 'api_version' in serviceSettings ? serviceSettings.api_version : undefined;
return [resourceName, deploymentId, apiVersion].filter(Boolean).join(', ');
}
function mistralAttributes(endpoint: InferenceAPIConfigResponse) {
const serviceSettings = endpoint.service_settings;
const maxInputTokens =
'max_input_tokens' in serviceSettings ? serviceSettings.max_input_tokens : undefined;
const rateLimit =
'rate_limit' in serviceSettings ? serviceSettings.rate_limit.requests_per_minute : undefined;
return [
maxInputTokens && `max_input_tokens: ${maxInputTokens}`,
rateLimit && `rate_limit: ${rateLimit}`,
]
.filter(Boolean)
.join(', ');
}
function googleAIStudioAttributes(endpoint: InferenceAPIConfigResponse) {
const serviceSettings = endpoint.service_settings;
const rateLimit =
'rate_limit' in serviceSettings ? serviceSettings.rate_limit.requests_per_minute : undefined;
return rateLimit && `rate_limit: ${rateLimit}`;
}

View file

@ -0,0 +1,21 @@
/*
* 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 { EuiBadge, useEuiTheme } from '@elastic/eui';
interface ModelBadgeProps {
model?: string;
}
export const ModelBadge: React.FC<ModelBadgeProps> = ({ model }) => {
const { euiTheme } = useEuiTheme();
if (!model) return null;
return <EuiBadge color={euiTheme.colors.body}>{model}</EuiBadge>;
};

View file

@ -0,0 +1,20 @@
/*
* 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 { i18n } from '@kbn/i18n';
export const THREADS = (numThreads: number) =>
i18n.translate('xpack.searchInferenceEndpoints.elasticsearch.threads', {
defaultMessage: 'Threads: {numThreads}',
values: { numThreads },
});
export const ALLOCATIONS = (numAllocations: number) =>
i18n.translate('xpack.searchInferenceEndpoints.elasticsearch.allocations', {
defaultMessage: 'Allocations: {numAllocations}',
values: { numAllocations },
});

View file

@ -0,0 +1,32 @@
/*
* 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 { render, screen } from '@testing-library/react';
import React from 'react';
import { ServiceProvider } from './service_provider';
import { ServiceProviderKeys } from '../../types';
jest.mock('../../../../assets/images/providers/elastic.svg', () => 'elasticIcon.svg');
jest.mock('../../../../assets/images/providers/hugging_face.svg', () => 'huggingFaceIcon.svg');
jest.mock('../../../../assets/images/providers/cohere.svg', () => 'cohereIcon.svg');
jest.mock('../../../../assets/images/providers/open_ai.svg', () => 'openAIIcon.svg');
describe('ServiceProvider component', () => {
it('renders Hugging Face icon and name when providerKey is hugging_face', () => {
render(<ServiceProvider providerKey={ServiceProviderKeys.hugging_face} />);
expect(screen.getByText('Hugging Face')).toBeInTheDocument();
const icon = screen.getByTestId('table-column-service-provider-hugging_face');
expect(icon).toBeInTheDocument();
});
it('renders Open AI icon and name when providerKey is openai', () => {
render(<ServiceProvider providerKey={ServiceProviderKeys.openai} />);
expect(screen.getByText('OpenAI')).toBeInTheDocument();
const icon = screen.getByTestId('table-column-service-provider-openai');
expect(icon).toBeInTheDocument();
});
});

View file

@ -0,0 +1,83 @@
/*
* 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 { EuiIcon } from '@elastic/eui';
import React from 'react';
import elasticIcon from '../../../../assets/images/providers/elastic.svg';
import huggingFaceIcon from '../../../../assets/images/providers/hugging_face.svg';
import cohereIcon from '../../../../assets/images/providers/cohere.svg';
import openAIIcon from '../../../../assets/images/providers/open_ai.svg';
import azureAIStudioIcon from '../../../../assets/images/providers/azure_ai_studio.svg';
import azureOpenAIIcon from '../../../../assets/images/providers/azure_open_ai.svg';
import googleAIStudioIcon from '../../../../assets/images/providers/google_ai_studio.svg';
import mistralIcon from '../../../../assets/images/providers/mistral.svg';
import { ServiceProviderKeys } from '../../types';
interface ServiceProviderProps {
providerKey: ServiceProviderKeys;
}
interface ServiceProviderRecord {
icon: string;
name: string;
}
export const SERVICE_PROVIDERS: Record<ServiceProviderKeys, ServiceProviderRecord> = {
[ServiceProviderKeys.azureaistudio]: {
icon: azureAIStudioIcon,
name: 'Azure AI Studio',
},
[ServiceProviderKeys.azureopenai]: {
icon: azureOpenAIIcon,
name: 'Azure OpenAI',
},
[ServiceProviderKeys.cohere]: {
icon: cohereIcon,
name: 'Cohere',
},
[ServiceProviderKeys.elasticsearch]: {
icon: elasticIcon,
name: 'Elasticsearch',
},
[ServiceProviderKeys.elser]: {
icon: elasticIcon,
name: 'ELSER',
},
[ServiceProviderKeys.googleaistudio]: {
icon: googleAIStudioIcon,
name: 'Google AI Studio',
},
[ServiceProviderKeys.hugging_face]: {
icon: huggingFaceIcon,
name: 'Hugging Face',
},
[ServiceProviderKeys.mistral]: {
icon: mistralIcon,
name: 'Mistral',
},
[ServiceProviderKeys.openai]: {
icon: openAIIcon,
name: 'OpenAI',
},
};
export const ServiceProvider: React.FC<ServiceProviderProps> = ({ providerKey }) => {
const provider = SERVICE_PROVIDERS[providerKey];
return provider ? (
<>
<EuiIcon
data-test-subj={`table-column-service-provider-${providerKey}`}
type={provider.icon}
style={{ marginRight: '8px' }}
/>
<span>{provider.name}</span>
</>
) : (
<span>{providerKey}</span>
);
};

View file

@ -0,0 +1,29 @@
/*
* 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 { render, screen } from '@testing-library/react';
import React from 'react';
import { TaskType } from './task_type';
import { TaskTypes } from '../../types';
describe('TaskType component', () => {
it.each([
[TaskTypes.completion, 'completion'],
[TaskTypes.sparse_embedding, 'sparse_embedding'],
[TaskTypes.text_embedding, 'text_embedding'],
])('renders the task type badge for %s', (taskType, expected) => {
render(<TaskType type={taskType} />);
const badge = screen.getByTestId(`table-column-task-type-${taskType}`);
expect(badge).toBeInTheDocument();
expect(badge).toHaveTextContent(expected);
});
it('returns null when type is null', () => {
const { container } = render(<TaskType />);
expect(container).toBeEmptyDOMElement();
});
});

View file

@ -0,0 +1,26 @@
/*
* 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 { EuiBadge } from '@elastic/eui';
import React from 'react';
import { TaskTypes } from '../../types';
interface TaskTypeProps {
type?: TaskTypes;
}
export const TaskType: React.FC<TaskTypeProps> = ({ type }) => {
if (type != null) {
return (
<EuiBadge data-test-subj={`table-column-task-type-${type}`} color="hollow">
{type}
</EuiBadge>
);
}
return null;
};

View file

@ -0,0 +1,79 @@
/*
* 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 { InferenceAPIConfigResponse } from '@kbn/ml-trained-models-utils';
import React from 'react';
import type { HorizontalAlignment } from '@elastic/eui';
import * as i18n from '../../../../common/translations';
import { useActions } from './render_actions/use_actions';
import { EndpointInfo } from './render_endpoint/endpoint_info';
import { ServiceProvider } from './render_service_provider/service_provider';
import { TaskType } from './render_task_type/task_type';
import { DeploymentStatus } from './render_deployment_status/deployment_status';
import { DeploymentStatusEnum, ServiceProviderKeys, TaskTypes } from '../types';
export const useTableColumns = () => {
const { actions } = useActions();
const deploymentAlignment: HorizontalAlignment = 'center';
const TABLE_COLUMNS = [
{
field: 'deployment',
name: '',
render: (deployment: DeploymentStatusEnum) => {
if (deployment != null) {
return <DeploymentStatus status={deployment} />;
}
return null;
},
width: '64px',
align: deploymentAlignment,
},
{
field: 'endpoint',
name: i18n.ENDPOINT,
render: (endpoint: InferenceAPIConfigResponse) => {
if (endpoint != null) {
return <EndpointInfo endpoint={endpoint} />;
}
return null;
},
sortable: true,
},
{
field: 'provider',
name: i18n.SERVICE_PROVIDER,
render: (provider: ServiceProviderKeys) => {
if (provider != null) {
return <ServiceProvider providerKey={provider} />;
}
return null;
},
sortable: false,
width: '265px',
},
{
field: 'type',
name: i18n.TASK_TYPE,
render: (type: TaskTypes) => {
if (type != null) {
return <TaskType type={type} />;
}
return null;
},
sortable: false,
width: '265px',
},
actions,
];
return TABLE_COLUMNS;
};

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 { render, screen, fireEvent } from '@testing-library/react';
import { TableSearch } from './table_search';
import React from 'react';
describe('TableSearchComponent', () => {
const mockSetSearchKey = jest.fn();
it('renders correctly', () => {
render(<TableSearch searchKey="" setSearchKey={mockSetSearchKey} />);
expect(screen.getByRole('searchbox')).toBeInTheDocument();
});
it('input value matches searchKey prop', () => {
render(<TableSearch searchKey="test" setSearchKey={mockSetSearchKey} />);
expect(screen.getByRole('searchbox')).toHaveValue('test');
});
it('calls setSearchKey on input change', () => {
render(<TableSearch searchKey="" setSearchKey={mockSetSearchKey} />);
fireEvent.change(screen.getByRole('searchbox'), { target: { value: 'new search' } });
expect(mockSetSearchKey).toHaveBeenCalledWith('new search');
});
});

View file

@ -0,0 +1,34 @@
/*
* 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 { EuiFieldSearch } from '@elastic/eui';
import React, { useCallback } from 'react';
interface TableSearchComponentProps {
searchKey: string;
setSearchKey: React.Dispatch<React.SetStateAction<string>>;
}
export const TableSearch: React.FC<TableSearchComponentProps> = ({ searchKey, setSearchKey }) => {
const onSearch = useCallback(
(newSearch) => {
const trimSearch = newSearch.trim();
setSearchKey(trimSearch);
},
[setSearchKey]
);
return (
<EuiFieldSearch
aria-label="Search endpoints"
placeholder="Search"
onChange={(e) => setSearchKey(e.target.value)}
onSearch={onSearch}
value={searchKey}
/>
);
};

View file

@ -1,35 +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 { i18n } from '@kbn/i18n';
export const TABLE_COLUMNS = [
{
field: 'endpoint',
name: i18n.translate('xpack.searchInferenceEndpoints.inferenceEndpoints.table.endpoint', {
defaultMessage: 'Endpoint',
}),
sortable: true,
width: '50%',
},
{
field: 'provider',
name: i18n.translate('xpack.searchInferenceEndpoints.inferenceEndpoints.table.provider', {
defaultMessage: 'Provider',
}),
sortable: false,
width: '110px',
},
{
field: 'type',
name: i18n.translate('xpack.searchInferenceEndpoints.inferenceEndpoints.table.type', {
defaultMessage: 'Type',
}),
sortable: false,
width: '90px',
},
];

View file

@ -36,6 +36,12 @@ const inferenceEndpoints = [
},
] as InferenceAPIConfigResponse[];
jest.mock('../../hooks/use_delete_endpoint', () => ({
useDeleteEndpoint: () => ({
mutate: jest.fn().mockImplementation(() => Promise.resolve()), // Mock implementation of the mutate function
}),
}));
describe('When the tabular page is loaded', () => {
beforeEach(() => {
render(<TabularPage inferenceEndpoints={inferenceEndpoints} />);

View file

@ -5,28 +5,87 @@
* 2.0.
*/
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { InferenceAPIConfigResponse } from '@kbn/ml-trained-models-utils';
import { extractErrorProperties } from '@kbn/ml-error-utils';
import * as i18n from '../../../common/translations';
import { useTableData } from '../../hooks/use_table_data';
import { FilterOptions } from './types';
import { DeploymentStatusEnum } from './types';
import { useAllInferenceEndpointsState } from '../../hooks/use_all_inference_endpoints_state';
import { EndpointsTable } from './endpoints_table';
import { TABLE_COLUMNS } from './table_columns';
import { ServiceProviderFilter } from './filter/service_provider_filter';
import { TaskTypeFilter } from './filter/task_type_filter';
import { TableSearch } from './search/table_search';
import { useTableColumns } from './render_table_columns/table_columns';
import { useKibana } from '../../hooks/use_kibana';
interface TabularPageProps {
inferenceEndpoints: InferenceAPIConfigResponse[];
}
export const TabularPage: React.FC<TabularPageProps> = ({ inferenceEndpoints }) => {
const { queryParams, setQueryParams } = useAllInferenceEndpointsState();
const [searchKey, setSearchKey] = React.useState('');
const [deploymentStatus, setDeploymentStatus] = React.useState<
Record<string, DeploymentStatusEnum>
>({});
const { queryParams, setQueryParams, filterOptions, setFilterOptions } =
useAllInferenceEndpointsState();
const {
services: { ml, notifications },
} = useKibana();
const onFilterChangedCallback = useCallback(
(newFilterOptions: Partial<FilterOptions>) => {
setFilterOptions(newFilterOptions);
},
[setFilterOptions]
);
useEffect(() => {
const fetchDeploymentStatus = async () => {
const trainedModelStats = await ml?.mlApi?.trainedModels.getTrainedModelStats();
if (trainedModelStats) {
const newDeploymentStatus = trainedModelStats?.trained_model_stats.reduce(
(acc, modelStat) => {
if (modelStat.model_id) {
acc[modelStat.model_id] =
modelStat?.deployment_stats?.state === 'started'
? DeploymentStatusEnum.deployed
: DeploymentStatusEnum.notDeployed;
}
return acc;
},
{} as Record<string, DeploymentStatusEnum>
);
setDeploymentStatus(newDeploymentStatus);
}
};
fetchDeploymentStatus().catch((error) => {
const errorObj = extractErrorProperties(error);
notifications?.toasts?.addError(errorObj.message ? new Error(error.message) : error, {
title: i18n.TRAINED_MODELS_STAT_GATHER_FAILED,
});
});
}, [ml, notifications]);
const { paginatedSortedTableData, pagination, sorting } = useTableData(
inferenceEndpoints,
queryParams
queryParams,
filterOptions,
searchKey,
deploymentStatus
);
const tableColumns = useTableColumns();
const handleTableChange = useCallback(
({ page, sort }) => {
const newQueryParams = {
@ -46,12 +105,32 @@ export const TabularPage: React.FC<TabularPageProps> = ({ inferenceEndpoints })
);
return (
<EndpointsTable
columns={TABLE_COLUMNS}
data={paginatedSortedTableData}
onChange={handleTableChange}
pagination={pagination}
sorting={sorting}
/>
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiFlexGroup gutterSize="s">
<EuiFlexItem style={{ width: '400px' }} grow={false}>
<TableSearch searchKey={searchKey} setSearchKey={setSearchKey} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<ServiceProviderFilter
optionKeys={filterOptions.provider}
onChange={onFilterChangedCallback}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<TaskTypeFilter optionKeys={filterOptions.type} onChange={onFilterChangedCallback} />
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EndpointsTable
columns={tableColumns}
data={paginatedSortedTableData}
onChange={handleTableChange}
pagination={pagination}
sorting={sorting}
/>
</EuiFlexItem>
</EuiFlexGroup>
);
};

View file

@ -5,8 +5,28 @@
* 2.0.
*/
import { InferenceAPIConfigResponse } from '@kbn/ml-trained-models-utils';
export const INFERENCE_ENDPOINTS_TABLE_PER_PAGE_VALUES = [10, 25, 50, 100];
export enum ServiceProviderKeys {
azureopenai = 'azureopenai',
azureaistudio = 'azureaistudio',
cohere = 'cohere',
elasticsearch = 'elasticsearch',
elser = 'elser',
googleaistudio = 'googleaistudio',
hugging_face = 'hugging_face',
mistral = 'mistral',
openai = 'openai',
}
export enum TaskTypes {
completion = 'completion',
rerank = 'rerank',
sparse_embedding = 'sparse_embedding',
text_embedding = 'text_embedding',
}
export enum SortFieldInferenceEndpoint {
endpoint = 'endpoint',
}
@ -25,7 +45,13 @@ export interface QueryParams extends SortingParams {
perPage: number;
}
export interface AlInferenceEndpointsTableState {
export interface FilterOptions {
provider: ServiceProviderKeys[];
type: TaskTypes[];
}
export interface AllInferenceEndpointsTableState {
filterOptions: FilterOptions;
queryParams: QueryParams;
}
@ -34,8 +60,16 @@ export interface EuiBasicTableSortTypes {
field: string;
}
export enum DeploymentStatusEnum {
deployed = 'deployed',
notDeployed = 'not_deployed',
notDeployable = 'not_deployable',
notApplicable = 'not_applicable',
}
export interface InferenceEndpointUI {
endpoint: string;
deployment: DeploymentStatusEnum;
endpoint: InferenceAPIConfigResponse;
provider: string;
type: string;
}

View file

@ -9,7 +9,7 @@ import React from 'react';
import {
EuiButton,
EuiEmptyPrompt,
EuiPageTemplate,
EuiFlexGroup,
EuiFlexItem,
EuiImage,
@ -20,8 +20,7 @@ import * as i18n from '../../../common/translations';
import inferenceEndpoint from '../../assets/images/inference_endpoint.svg';
import { ElserPrompt } from './elser_prompt';
import { MultilingualE5Prompt } from './multilingual_e5_prompt';
import { EndpointPrompt } from './endpoint_prompt';
import './add_empty_prompt.scss';
@ -31,9 +30,12 @@ interface AddEmptyPromptProps {
export const AddEmptyPrompt: React.FC<AddEmptyPromptProps> = ({ setIsInferenceFlyoutVisible }) => {
return (
<EuiEmptyPrompt
className="addEmptyPrompt"
<EuiPageTemplate.EmptyPrompt
layout="horizontal"
restrictWidth
color="plain"
hasShadow
icon={<EuiImage size="fullWidth" src={inferenceEndpoint} alt="" />}
title={<h2>{i18n.INFERENCE_ENDPOINT_LABEL}</h2>}
body={
<EuiFlexGroup direction="column">
@ -60,20 +62,41 @@ export const AddEmptyPrompt: React.FC<AddEmptyPromptProps> = ({ setIsInferenceFl
<EuiFlexItem>
<strong>{i18n.START_WITH_PREPARED_ENDPOINTS_LABEL}</strong>
</EuiFlexItem>
<EuiSpacer />
<EuiSpacer size="s" />
<EuiFlexGroup>
<EuiFlexItem>
<ElserPrompt setIsInferenceFlyoutVisible={setIsInferenceFlyoutVisible} />
<EndpointPrompt
setIsInferenceFlyoutVisible={setIsInferenceFlyoutVisible}
title={i18n.ELSER_TITLE}
description={i18n.ELSER_DESCRIPTION}
footer={
<EuiButton
iconType="plusInCircle"
onClick={() => setIsInferenceFlyoutVisible(true)}
>
{i18n.ADD_ENDPOINT_LABEL}
</EuiButton>
}
/>
</EuiFlexItem>
<EuiFlexItem>
<MultilingualE5Prompt setIsInferenceFlyoutVisible={setIsInferenceFlyoutVisible} />
<EndpointPrompt
setIsInferenceFlyoutVisible={setIsInferenceFlyoutVisible}
title={i18n.E5_TITLE}
description={i18n.E5_DESCRIPTION}
footer={
<EuiButton
iconType="plusInCircle"
onClick={() => setIsInferenceFlyoutVisible(true)}
>
{i18n.ADD_ENDPOINT_LABEL}
</EuiButton>
}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexGroup>
}
color="plain"
hasBorder
icon={<EuiImage size="fullWidth" src={inferenceEndpoint} alt="" />}
/>
);
};

View file

@ -1,31 +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 { EuiButton, EuiCard } from '@elastic/eui';
import * as i18n from '../../../common/translations';
interface ElserPromptProps {
setIsInferenceFlyoutVisible: (value: boolean) => void;
}
export const ElserPrompt: React.FC<ElserPromptProps> = ({ setIsInferenceFlyoutVisible }) => (
<EuiCard
display="plain"
hasBorder
textAlign="left"
data-test-subj="elserPromptForEmptyState"
title={i18n.ELSER_TITLE}
description={i18n.ELSER_DESCRIPTION}
footer={
<EuiButton iconType="plusInCircle" onClick={() => setIsInferenceFlyoutVisible(true)}>
{i18n.ADD_ENDPOINT_LABEL}
</EuiButton>
}
/>
);

View file

@ -6,29 +6,28 @@
*/
import React from 'react';
import { EuiCard } from '@elastic/eui';
import { EuiButton, EuiCard } from '@elastic/eui';
import * as i18n from '../../../common/translations';
interface MultilingualE5PromptProps {
interface EndpointPromptProps {
setIsInferenceFlyoutVisible: (value: boolean) => void;
title: string;
description: string;
footer: React.ReactElement;
}
export const MultilingualE5Prompt: React.FC<MultilingualE5PromptProps> = ({
export const EndpointPrompt: React.FC<EndpointPromptProps> = ({
setIsInferenceFlyoutVisible,
title,
description,
footer,
}) => (
<EuiCard
display="plain"
hasBorder
textAlign="left"
data-test-subj="multilingualE5PromptForEmptyState"
title={i18n.E5_TITLE}
description={i18n.E5_DESCRIPTION}
footer={
<EuiButton iconType="plusInCircle" onClick={() => setIsInferenceFlyoutVisible(true)}>
{i18n.ADD_ENDPOINT_LABEL}
</EuiButton>
}
title={title}
titleSize="xs"
description={description}
footer={footer}
/>
);

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import { EuiButton, EuiPageTemplate } from '@elastic/eui';
import React from 'react';
import { EuiPageTemplate, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui';
import * as i18n from '../../common/translations';
interface InferenceEndpointsHeaderProps {
@ -21,21 +21,18 @@ export const InferenceEndpointsHeader: React.FC<InferenceEndpointsHeaderProps> =
data-test-subj="allInferenceEndpointsPage"
pageTitle={i18n.INFERENCE_ENDPOINT_LABEL}
description={i18n.MANAGE_INFERENCE_ENDPOINTS_LABEL}
bottomBorder={true}
rightSideItems={[
<EuiFlexGroup gutterSize="xs">
<EuiFlexItem>
<EuiButton
key="newInferenceEndpoint"
color="primary"
iconType="plusInCircle"
data-test-subj="addEndpointButtonForAllInferenceEndpoints"
fill
onClick={() => setIsInferenceFlyoutVisible(true)}
>
{i18n.ADD_ENDPOINT_LABEL}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>,
<EuiButton
key="newInferenceEndpoint"
color="primary"
iconType="plusInCircle"
data-test-subj="addEndpointButtonForAllInferenceEndpoints"
fill
onClick={() => setIsInferenceFlyoutVisible(true)}
>
{i18n.ADD_ENDPOINT_LABEL}
</EuiButton>,
]}
/>
);

View file

@ -47,9 +47,11 @@ export const InferenceFlyoutWrapperComponent: React.FC<InferenceFlyoutWrapperCom
const queryClient = useQueryClient();
const {
services: { ml },
services: { ml, notifications },
} = useKibana();
const toasts = notifications?.toasts;
const createInferenceEndpointMutation = useMutation(
async ({
inferenceId,
@ -64,6 +66,10 @@ export const InferenceFlyoutWrapperComponent: React.FC<InferenceFlyoutWrapperCom
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: () => {
@ -87,17 +93,26 @@ export const InferenceFlyoutWrapperComponent: React.FC<InferenceFlyoutWrapperCom
const onSaveInferenceCallback = useCallback(
async (inferenceId: string, taskType: InferenceTaskType, modelConfig: ModelConfig) => {
setIsCreateInferenceApiLoading(true);
try {
await createInferenceEndpointMutation.mutateAsync({ inferenceId, taskType, modelConfig });
setIsInferenceFlyoutVisible(!isInferenceFlyoutVisible);
} catch (error) {
const errorObj = extractErrorProperties(error);
setInferenceAddError(errorObj.message);
} finally {
setIsCreateInferenceApiLoading(false);
}
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]
[
createInferenceEndpointMutation,
isInferenceFlyoutVisible,
setIsInferenceFlyoutVisible,
notifications,
]
);
const onFlyoutClose = useCallback(() => {

View file

@ -0,0 +1,23 @@
/*
* 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 { i18n } from '@kbn/i18n';
export * from '../../common/translations';
export const ENDPOINT_DELETION_FAILED = i18n.translate(
'xpack.searchInferenceEndpoints.deleteEndpoint.endpointDeletionFailed',
{
defaultMessage: 'Endpoint deletion failed',
}
);
export const DELETE_SUCCESS = i18n.translate(
'xpack.searchInferenceEndpoints.deleteEndpoint.deleteSuccess',
{
defaultMessage: 'The inference endpoint has been deleted sucessfully.',
}
);

View file

@ -9,7 +9,8 @@ import { useCallback, useState } from 'react';
import type {
QueryParams,
AlInferenceEndpointsTableState,
AllInferenceEndpointsTableState,
FilterOptions,
} from '../components/all_inference_endpoints/types';
import { DEFAULT_INFERENCE_ENDPOINTS_TABLE_STATE } from '../components/all_inference_endpoints/constants';
@ -17,13 +18,15 @@ import { DEFAULT_INFERENCE_ENDPOINTS_TABLE_STATE } from '../components/all_infer
interface UseAllInferenceEndpointsStateReturn {
queryParams: QueryParams;
setQueryParams: (queryParam: Partial<QueryParams>) => void;
filterOptions: FilterOptions;
setFilterOptions: (filterOptions: Partial<FilterOptions>) => void;
}
export function useAllInferenceEndpointsState(): UseAllInferenceEndpointsStateReturn {
const [tableState, setTableState] = useState<AlInferenceEndpointsTableState>(
const [tableState, setTableState] = useState<AllInferenceEndpointsTableState>(
DEFAULT_INFERENCE_ENDPOINTS_TABLE_STATE
);
const setState = useCallback((state: AlInferenceEndpointsTableState) => {
const setState = useCallback((state: AllInferenceEndpointsTableState) => {
setTableState(state);
}, []);
@ -34,8 +37,19 @@ export function useAllInferenceEndpointsState(): UseAllInferenceEndpointsStateRe
},
setQueryParams: (newQueryParams: Partial<QueryParams>) => {
setState({
filterOptions: tableState.filterOptions,
queryParams: { ...tableState.queryParams, ...newQueryParams },
});
},
filterOptions: {
...DEFAULT_INFERENCE_ENDPOINTS_TABLE_STATE.filterOptions,
...tableState.filterOptions,
},
setFilterOptions: (newFilterOptions: Partial<FilterOptions>) => {
setState({
filterOptions: { ...tableState.filterOptions, ...newFilterOptions },
queryParams: tableState.queryParams,
});
},
};
}

View file

@ -0,0 +1,72 @@
/*
* 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-hooks';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useKibana } from './use_kibana';
import { useDeleteEndpoint } from './use_delete_endpoint';
import * as i18n from './translations';
import React from 'react';
jest.mock('./use_kibana');
const mockUseKibana = useKibana as jest.Mock;
const mockDelete = jest.fn();
const mockAddSuccess = jest.fn();
const mockAddError = jest.fn();
describe('useDeleteEndpoint', () => {
beforeEach(() => {
mockUseKibana.mockReturnValue({
services: {
http: {
delete: mockDelete,
},
notifications: {
toasts: {
addSuccess: mockAddSuccess,
addError: mockAddError,
},
},
},
});
mockDelete.mockResolvedValue({});
});
const wrapper = ({ children }: { children: React.ReactNode }) => {
const queryClient = new QueryClient();
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
};
it('should call delete endpoint and show success toast on success', async () => {
const { result, waitFor } = renderHook(() => useDeleteEndpoint(), { wrapper });
result.current.mutate({ type: 'text_embedding', id: 'in-1' });
await waitFor(() =>
expect(mockDelete).toHaveBeenCalledWith(
'/internal/inference_endpoint/endpoints/text_embedding/in-1'
)
);
expect(mockAddSuccess).toHaveBeenCalledWith({
title: i18n.DELETE_SUCCESS,
});
});
it('should show error toast on failure', async () => {
const error = new Error('Deletion failed');
mockDelete.mockRejectedValue(error);
const { result, waitFor } = renderHook(() => useDeleteEndpoint(), { wrapper });
result.current.mutate({ type: 'model', id: '123' });
await waitFor(() => expect(mockAddError).toHaveBeenCalled());
expect(mockAddError).toHaveBeenCalledWith(error, {
title: i18n.ENDPOINT_DELETION_FAILED,
});
});
});

View file

@ -0,0 +1,42 @@
/*
* 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 { useMutation, useQueryClient } from '@tanstack/react-query';
import { useKibana } from './use_kibana';
import * as i18n from './translations';
import { INFERENCE_ENDPOINTS_QUERY_KEY } from '../../common/constants';
interface MutationArgs {
type: string;
id: string;
}
export const useDeleteEndpoint = () => {
const queryClient = useQueryClient();
const { services } = useKibana();
const toasts = services.notifications?.toasts;
return useMutation(
async ({ type, id }: MutationArgs) => {
await services.http.delete<{}>(`/internal/inference_endpoint/endpoints/${type}/${id}`);
},
{
onSuccess: () => {
queryClient.invalidateQueries([INFERENCE_ENDPOINTS_QUERY_KEY]);
toasts?.addSuccess({
title: i18n.DELETE_SUCCESS,
});
},
onError: (error: Error) => {
toasts?.addError(new Error(error?.message), {
title: i18n.ENDPOINT_DELETION_FAILED,
});
},
}
);
};

View file

@ -36,8 +36,8 @@ const inferenceEndpoints = [
},
{
model_id: 'my-elser-model-05',
task_type: 'sparse_embedding',
service: 'elser',
task_type: 'text_embedding',
service: 'elasticsearch',
service_settings: {
num_allocations: 1,
num_threads: 1,
@ -54,9 +54,23 @@ const queryParams = {
sortOrder: 'desc',
} as QueryParams;
const filterOptions = {
provider: ['elser', 'elasticsearch'],
type: ['sparse_embedding', 'text_embedding'],
} as any;
const deploymentStatus = {
'.elser_model_2': 'deployed',
lang_ident_model_1: 'not_deployed',
} as any;
const searchKey = 'my';
describe('useTableData', () => {
it('should return correct pagination', () => {
const { result } = renderHook(() => useTableData(inferenceEndpoints, queryParams));
const { result } = renderHook(() =>
useTableData(inferenceEndpoints, queryParams, filterOptions, searchKey, deploymentStatus)
);
expect(result.current.pagination).toEqual({
pageIndex: 0,
@ -67,7 +81,9 @@ describe('useTableData', () => {
});
it('should return correct sorting', () => {
const { result } = renderHook(() => useTableData(inferenceEndpoints, queryParams));
const { result } = renderHook(() =>
useTableData(inferenceEndpoints, queryParams, filterOptions, searchKey, deploymentStatus)
);
expect(result.current.sorting).toEqual({
sort: {
@ -78,15 +94,54 @@ describe('useTableData', () => {
});
it('should return correctly sorted data', () => {
const { result } = renderHook(() => useTableData(inferenceEndpoints, queryParams));
const { result } = renderHook(() =>
useTableData(inferenceEndpoints, queryParams, filterOptions, searchKey, deploymentStatus)
);
const expectedSortedData = [...inferenceEndpoints].sort((a, b) =>
b.model_id.localeCompare(a.model_id)
);
const sortedEndpoints = result.current.sortedTableData.map((item) => item.endpoint);
const sortedEndpoints = result.current.sortedTableData.map((item) => item.endpoint.model_id);
const expectedModelIds = expectedSortedData.map((item) => item.model_id);
expect(sortedEndpoints).toEqual(expectedModelIds);
});
it('should filter data based on provider and type from filterOptions', () => {
const filterOptions2 = {
provider: ['elser'],
type: ['text_embedding'],
} as any;
const { result } = renderHook(() =>
useTableData(inferenceEndpoints, queryParams, filterOptions2, searchKey, deploymentStatus)
);
const filteredData = result.current.sortedTableData;
expect(
filteredData.every(
(endpoint) =>
filterOptions.provider.includes(endpoint.provider) &&
filterOptions.type.includes(endpoint.type)
)
).toBeTruthy();
});
it('should filter data based on searchKey', () => {
const searchKey2 = 'model-05';
const { result } = renderHook(() =>
useTableData(inferenceEndpoints, queryParams, filterOptions, searchKey2, deploymentStatus)
);
const filteredData = result.current.sortedTableData;
expect(filteredData.every((item) => item.endpoint.model_id.includes(searchKey))).toBeTruthy();
});
it('should update deployment status based on deploymentStatus object', () => {
const { result } = renderHook(() =>
useTableData(inferenceEndpoints, queryParams, filterOptions, searchKey, deploymentStatus)
);
const updatedData = result.current.sortedTableData;
expect(updatedData[0].deployment).toEqual('deployed');
});
});

View file

@ -11,11 +11,15 @@ import { InferenceAPIConfigResponse } from '@kbn/ml-trained-models-utils';
import { useMemo } from 'react';
import { DEFAULT_TABLE_LIMIT } from '../components/all_inference_endpoints/constants';
import {
InferenceEndpointUI,
FilterOptions,
INFERENCE_ENDPOINTS_TABLE_PER_PAGE_VALUES,
InferenceEndpointUI,
QueryParams,
SortOrder,
ServiceProviderKeys,
TaskTypes,
} from '../components/all_inference_endpoints/types';
import { DeploymentStatusEnum } from '../components/all_inference_endpoints/types';
interface UseTableDataReturn {
tableData: InferenceEndpointUI[];
@ -27,15 +31,50 @@ interface UseTableDataReturn {
export const useTableData = (
inferenceEndpoints: InferenceAPIConfigResponse[],
queryParams: QueryParams
queryParams: QueryParams,
filterOptions: FilterOptions,
searchKey: string,
deploymentStatus: Record<string, DeploymentStatusEnum>
): UseTableDataReturn => {
const tableData: InferenceEndpointUI[] = useMemo(() => {
return inferenceEndpoints.map((endpoint) => ({
endpoint: endpoint.model_id,
provider: endpoint.service,
type: endpoint.task_type,
}));
}, [inferenceEndpoints]);
let filteredEndpoints = inferenceEndpoints;
if (filterOptions.provider.length > 0) {
filteredEndpoints = filteredEndpoints.filter((endpoint) =>
filterOptions.provider.includes(ServiceProviderKeys[endpoint.service])
);
}
if (filterOptions.type.length > 0) {
filteredEndpoints = filteredEndpoints.filter((endpoint) =>
filterOptions.type.includes(TaskTypes[endpoint.task_type])
);
}
return filteredEndpoints
.filter((endpoint) => endpoint.model_id.includes(searchKey))
.map((endpoint) => {
const isElasticService =
endpoint.service === ServiceProviderKeys.elasticsearch ||
endpoint.service === ServiceProviderKeys.elser;
let deploymentStatusValue = DeploymentStatusEnum.notApplicable;
if (isElasticService) {
const modelId = endpoint.service_settings?.model_id;
deploymentStatusValue =
modelId && deploymentStatus[modelId] !== undefined
? deploymentStatus[modelId]
: DeploymentStatusEnum.notDeployable;
}
return {
deployment: deploymentStatusValue,
endpoint,
provider: endpoint.service,
type: endpoint.task_type,
};
});
}, [inferenceEndpoints, searchKey, filterOptions, deploymentStatus]);
const sortedTableData: InferenceEndpointUI[] = useMemo(() => {
return [...tableData].sort((a, b) => {
@ -43,9 +82,9 @@ export const useTableData = (
const bValue = b[queryParams.sortField];
if (queryParams.sortOrder === SortOrder.asc) {
return aValue.localeCompare(bValue);
return aValue.model_id.localeCompare(bValue.model_id);
} else {
return bValue.localeCompare(aValue);
return bValue.model_id.localeCompare(aValue.model_id);
}
});
}, [tableData, queryParams]);

View file

@ -0,0 +1,32 @@
/*
* 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 { deleteInferenceEndpoint } from './delete_inference_endpoint';
describe('deleteInferenceEndpoint', () => {
let mockClient: any;
beforeEach(() => {
mockClient = {
transport: {
request: jest.fn(),
},
};
});
it('should call the Elasticsearch client with the correct DELETE request', async () => {
const type = 'model';
const id = 'model-id-123';
await deleteInferenceEndpoint(mockClient, type, id);
expect(mockClient.transport.request).toHaveBeenCalledWith({
method: 'DELETE',
path: `/_inference/${type}/${id}`,
});
});
});

View file

@ -0,0 +1,19 @@
/*
* 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 { ElasticsearchClient } from '@kbn/core/server';
export const deleteInferenceEndpoint = async (
client: ElasticsearchClient,
type: string,
id: string
): Promise<void> => {
return await client.transport.request({
method: 'DELETE',
path: `/_inference/${type}/${id}`,
});
};

View file

@ -6,10 +6,12 @@
*/
import { IRouter } from '@kbn/core/server';
import { schema } from '@kbn/config-schema';
import type { Logger } from '@kbn/logging';
import { fetchInferenceEndpoints } from './lib/fetch_inference_endpoints';
import { APIRoutes } from './types';
import { errorHandler } from './utils/error_handler';
import { deleteInferenceEndpoint } from './lib/delete_inference_endpoint';
export function defineRoutes({ logger, router }: { logger: Logger; router: IRouter }) {
router.get(
@ -32,4 +34,27 @@ export function defineRoutes({ logger, router }: { logger: Logger; router: IRout
});
})
);
router.delete(
{
path: APIRoutes.DELETE_INFERENCE_ENDPOINT,
validate: {
params: schema.object({
type: schema.string(),
id: schema.string(),
}),
},
},
errorHandler(logger)(async (context, request, response) => {
const {
client: { asCurrentUser },
} = (await context.core).elasticsearch;
const { type, id } = request.params;
await deleteInferenceEndpoint(asCurrentUser, type, id);
return response.ok();
})
);
}