From ee7fd95214ab74f2d2ba6c8e3cd076e7fdffdbfc Mon Sep 17 00:00:00 2001 From: Saikat Sarkar <132922331+saikatsarkar056@users.noreply.github.com> Date: Tue, 9 Jul 2024 12:10:54 -0600 Subject: [PATCH] [8.15] [Inference Endpoints View] Deletion, search and filtering of inference endpoints (#186206) (#187887) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 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) ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) --- packages/deeplinks/search/constants.ts | 2 +- packages/deeplinks/search/deep_links.ts | 8 +- packages/deeplinks/search/index.ts | 2 +- .../collectors/application_usage/schema.ts | 2 +- src/plugins/telemetry/schema/oss_plugins.json | 2 +- .../src/constants/trained_models.ts | 46 ++++ .../enterprise_search/common/constants.ts | 4 +- .../kea_logic/licensing_logic.mock.ts | 1 + .../public/applications/shared/layout/nav.tsx | 48 ++-- .../shared/licensing/licensing_logic.test.ts | 32 +++ .../shared/licensing/licensing_logic.ts | 8 + .../public/navigation_tree.ts | 2 +- .../enterprise_search/public/plugin.ts | 62 +++-- .../enterprise_search/server/plugin.ts | 4 + .../common/translations.ts | 64 +++++ .../common/types.ts | 1 + .../images/providers/azure_ai_studio.svg | 44 +++ .../assets/images/providers/azure_open_ai.svg | 9 + .../public/assets/images/providers/cohere.svg | 9 + .../assets/images/providers/elastic.svg | 16 ++ .../images/providers/google_ai_studio.svg | 6 + .../assets/images/providers/hugging_face.svg | 10 + .../assets/images/providers/mistral.svg | 34 +++ .../assets/images/providers/open_ai.svg | 3 + .../all_inference_endpoints/constants.ts | 11 +- .../filter/multi_select_filter.test.tsx | 66 +++++ .../filter/multi_select_filter.tsx | 108 ++++++++ .../filter/service_provider_filter.tsx | 43 +++ .../filter/task_type_filter.tsx | 42 +++ .../filter/translations.ts | 22 ++ .../copy_id/use_copy_id_action.test.tsx | 73 +++++ .../actions/copy_id/use_copy_id_action.tsx | 44 +++ .../confirm_delete_endpoint/index.test.tsx | 41 +++ .../delete/confirm_delete_endpoint/index.tsx | 34 +++ .../confirm_delete_endpoint/translations.ts | 24 ++ .../actions/delete/use_delete_action.tsx | 55 ++++ .../render_actions/actions/types.ts | 12 + .../render_actions/use_actions.tsx | 79 ++++++ .../deployment_status.test.tsx | 27 ++ .../deployment_status.tsx | 52 ++++ .../render_deployment_status/translations.ts | 29 ++ .../render_endpoint/endpoint_info.test.tsx | 257 ++++++++++++++++++ .../render_endpoint/endpoint_info.tsx | 164 +++++++++++ .../render_endpoint/model_badge.tsx | 21 ++ .../render_endpoint/translations.ts | 20 ++ .../service_provider.test.tsx | 32 +++ .../service_provider.tsx | 83 ++++++ .../render_task_type/task_type.test.tsx | 29 ++ .../render_task_type/task_type.tsx | 26 ++ .../render_table_columns/table_columns.tsx | 79 ++++++ .../search/table_search.test.tsx | 30 ++ .../search/table_search.tsx | 34 +++ .../all_inference_endpoints/table_columns.ts | 35 --- .../tabular_page.test.tsx | 6 + .../all_inference_endpoints/tabular_page.tsx | 101 ++++++- .../all_inference_endpoints/types.ts | 38 ++- .../empty_prompt/add_empty_prompt.tsx | 45 ++- .../components/empty_prompt/elser_prompt.tsx | 31 --- ...gual_e5_prompt.tsx => endpoint_prompt.tsx} | 27 +- .../components/inference_endpoints_header.tsx | 27 +- .../inference_flyout_wrapper_component.tsx | 37 ++- .../public/hooks/translations.ts | 23 ++ .../use_all_inference_endpoints_state.tsx | 20 +- .../public/hooks/use_delete_endpoint.test.tsx | 72 +++++ .../public/hooks/use_delete_endpoint.tsx | 42 +++ .../public/hooks/use_table_data.test.tsx | 67 ++++- .../public/hooks/use_table_data.tsx | 59 +++- .../lib/delete_inference_endpoint.test.ts | 32 +++ .../server/lib/delete_inference_endpoint.ts | 19 ++ .../server/routes.ts | 25 ++ 70 files changed, 2458 insertions(+), 204 deletions(-) create mode 100644 x-pack/plugins/search_inference_endpoints/public/assets/images/providers/azure_ai_studio.svg create mode 100644 x-pack/plugins/search_inference_endpoints/public/assets/images/providers/azure_open_ai.svg create mode 100644 x-pack/plugins/search_inference_endpoints/public/assets/images/providers/cohere.svg create mode 100644 x-pack/plugins/search_inference_endpoints/public/assets/images/providers/elastic.svg create mode 100644 x-pack/plugins/search_inference_endpoints/public/assets/images/providers/google_ai_studio.svg create mode 100644 x-pack/plugins/search_inference_endpoints/public/assets/images/providers/hugging_face.svg create mode 100644 x-pack/plugins/search_inference_endpoints/public/assets/images/providers/mistral.svg create mode 100644 x-pack/plugins/search_inference_endpoints/public/assets/images/providers/open_ai.svg create mode 100644 x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/multi_select_filter.test.tsx create mode 100644 x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/multi_select_filter.tsx create mode 100644 x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/service_provider_filter.tsx create mode 100644 x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/task_type_filter.tsx create mode 100644 x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/translations.ts create mode 100644 x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/copy_id/use_copy_id_action.test.tsx create mode 100644 x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/copy_id/use_copy_id_action.tsx create mode 100644 x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/index.test.tsx create mode 100644 x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/index.tsx create mode 100644 x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/translations.ts create mode 100644 x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/use_delete_action.tsx create mode 100644 x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/types.ts create mode 100644 x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/use_actions.tsx create mode 100644 x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_deployment_status/deployment_status.test.tsx create mode 100644 x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_deployment_status/deployment_status.tsx create mode 100644 x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_deployment_status/translations.ts create mode 100644 x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_endpoint/endpoint_info.test.tsx create mode 100644 x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_endpoint/endpoint_info.tsx create mode 100644 x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_endpoint/model_badge.tsx create mode 100644 x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_endpoint/translations.ts create mode 100644 x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_service_provider/service_provider.test.tsx create mode 100644 x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_service_provider/service_provider.tsx create mode 100644 x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_task_type/task_type.test.tsx create mode 100644 x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_task_type/task_type.tsx create mode 100644 x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/table_columns.tsx create mode 100644 x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/search/table_search.test.tsx create mode 100644 x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/search/table_search.tsx delete mode 100644 x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/table_columns.ts delete mode 100644 x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/elser_prompt.tsx rename x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/{multilingual_e5_prompt.tsx => endpoint_prompt.tsx} (51%) create mode 100644 x-pack/plugins/search_inference_endpoints/public/hooks/translations.ts create mode 100644 x-pack/plugins/search_inference_endpoints/public/hooks/use_delete_endpoint.test.tsx create mode 100644 x-pack/plugins/search_inference_endpoints/public/hooks/use_delete_endpoint.tsx create mode 100644 x-pack/plugins/search_inference_endpoints/server/lib/delete_inference_endpoint.test.ts create mode 100644 x-pack/plugins/search_inference_endpoints/server/lib/delete_inference_endpoint.ts diff --git a/packages/deeplinks/search/constants.ts b/packages/deeplinks/search/constants.ts index 3fdcc78bb68a1..8aa53658320f5 100644 --- a/packages/deeplinks/search/constants.ts +++ b/packages/deeplinks/search/constants.ts @@ -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'; diff --git a/packages/deeplinks/search/deep_links.ts b/packages/deeplinks/search/deep_links.ts index f004d1b2c9dd6..4b32ec9757bde 100644 --- a/packages/deeplinks/search/deep_links.ts +++ b/packages/deeplinks/search/deep_links.ts @@ -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}`; diff --git a/packages/deeplinks/search/index.ts b/packages/deeplinks/search/index.ts index 663d625e9fd72..a18f0cb31426f 100644 --- a/packages/deeplinks/search/index.ts +++ b/packages/deeplinks/search/index.ts @@ -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, diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts index 4ffe9d483a1a4..8d6343337f152 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts @@ -135,7 +135,7 @@ export const applicationUsageSchema = { canvas: commonSchema, enterpriseSearch: commonSchema, enterpriseSearchContent: commonSchema, - enterpriseSearchInferenceEndpoints: commonSchema, + enterpriseSearchRelevance: commonSchema, enterpriseSearchAnalytics: commonSchema, enterpriseSearchApplications: commonSchema, enterpriseSearchAISearch: commonSchema, diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 1e2c942ba2a5c..56c2be139fb06 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -2098,7 +2098,7 @@ } } }, - "enterpriseSearchInferenceEndpoints": { + "enterpriseSearchRelevance": { "properties": { "appId": { "type": "keyword", diff --git a/x-pack/packages/ml/trained_models_utils/src/constants/trained_models.ts b/x-pack/packages/ml/trained_models_utils/src/constants/trained_models.ts index 0e19fef36e2e2..49acb6e1d5169 100644 --- a/x-pack/packages/ml/trained_models_utils/src/constants/trained_models.ts +++ b/x-pack/packages/ml/trained_models_utils/src/constants/trained_models.ts @@ -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; + }; }; } | { diff --git a/x-pack/plugins/enterprise_search/common/constants.ts b/x-pack/plugins/enterprise_search/common/constants.ts index 47c4741e41afc..f2be720d1c04c 100644 --- a/x-pack/plugins/enterprise_search/common/constants.ts +++ b/x-pack/plugins/enterprise_search/common/constants.ts @@ -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', }), diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/licensing_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/licensing_logic.mock.ts index 0ea858d0f2f50..4f91ddf4cb5c6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/licensing_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/licensing_logic.mock.ts @@ -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, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx index 77454581c61e7..e39f0f0b71f29 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.tsx @@ -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> = [ @@ -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: [ diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts index 85a26abeef1e1..743483e96fa31 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.test.ts @@ -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' }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts index 77a09de2c863f..ab3586f6563c1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/licensing_logic.ts @@ -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 [selectors.license], + (license) => { + const qualifyingLicenses = ['enterprise', 'trial']; + return license?.isActive && qualifyingLicenses.includes(license?.type); + }, + ], hasGoldLicense: [ (selectors) => [selectors.license], (license) => { diff --git a/x-pack/plugins/enterprise_search/public/navigation_tree.ts b/x-pack/plugins/enterprise_search/public/navigation_tree.ts index 8bb6bf70e603b..d5c640fa67b3e 100644 --- a/x-pack/plugins/enterprise_search/public/navigation_tree.ts +++ b/x-pack/plugins/enterprise_search/public/navigation_tree.ts @@ -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', diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index 280de2f04356b..552bb43fbd073 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -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(); @@ -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); - - await this.getInitialData(http); - const pluginData = this.getPluginData(); - - const { renderApp } = await import('./applications'); - const { EnterpriseSearchRelevance } = await import( - './applications/enterprise_search_relevance' - ); - - return renderApp(EnterpriseSearchRelevance, kibanaDeps, pluginData); - }, - title: INFERENCE_ENDPOINTS_PLUGIN.NAME, - visibleIn: [], + 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(); + + const { renderApp } = await import('./applications'); + const { EnterpriseSearchRelevance } = await import( + './applications/enterprise_search_relevance' + ); + + 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) => { this.sideNavDynamicItems$.next({ ...this.sideNavDynamicItems$.getValue(), ...items }); diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index 24298c55fdffa..ee403e223305f 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -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); diff --git a/x-pack/plugins/search_inference_endpoints/common/translations.ts b/x-pack/plugins/search_inference_endpoints/common/translations.ts index e58829812829b..8171b8bba0254 100644 --- a/x-pack/plugins/search_inference_endpoints/common/translations.ts +++ b/x-pack/plugins/search_inference_endpoints/common/translations.ts @@ -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', + } +); diff --git a/x-pack/plugins/search_inference_endpoints/common/types.ts b/x-pack/plugins/search_inference_endpoints/common/types.ts index ef29d987309f8..e6529de4e3a64 100644 --- a/x-pack/plugins/search_inference_endpoints/common/types.ts +++ b/x-pack/plugins/search_inference_endpoints/common/types.ts @@ -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 { diff --git a/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/azure_ai_studio.svg b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/azure_ai_studio.svg new file mode 100644 index 0000000000000..405e182a10394 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/azure_ai_studio.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/azure_open_ai.svg b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/azure_open_ai.svg new file mode 100644 index 0000000000000..122c0c65af13c --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/azure_open_ai.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/cohere.svg b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/cohere.svg new file mode 100644 index 0000000000000..69953809fec35 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/cohere.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/elastic.svg b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/elastic.svg new file mode 100644 index 0000000000000..e763c2e2f2ab6 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/elastic.svg @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/google_ai_studio.svg b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/google_ai_studio.svg new file mode 100644 index 0000000000000..b6e34ae15c9e4 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/google_ai_studio.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/hugging_face.svg b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/hugging_face.svg new file mode 100644 index 0000000000000..87ac70c5a18f4 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/hugging_face.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/mistral.svg b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/mistral.svg new file mode 100644 index 0000000000000..f62258a327594 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/mistral.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/open_ai.svg b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/open_ai.svg new file mode 100644 index 0000000000000..9ddc8f8fd63b8 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/assets/images/providers/open_ai.svg @@ -0,0 +1,3 @@ + + + diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/constants.ts b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/constants.ts index 1b7e72149fd43..b3fd13dc5383a 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/constants.ts +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/constants.ts @@ -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, }; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/multi_select_filter.test.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/multi_select_filter.test.tsx new file mode 100644 index 0000000000000..87b984c26d3ea --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/multi_select_filter.test.tsx @@ -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( + {}} 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( + {}} 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( + {}} 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( + + ); + + fireEvent.click(getByText('Filter Options')); + fireEvent.click(getByText('Option 1')); + + await waitFor(() => { + expect(onChange).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/multi_select_filter.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/multi_select_filter.tsx new file mode 100644 index 0000000000000..84883c4e85432 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/multi_select_filter.tsx @@ -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 = ({ + 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 ( + + 0} + numActiveFilters={selectedOptionKeys.length} + aria-label={buttonLabel} + > + + {buttonLabel} + + + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + panelPaddingSize="none" + repositionOnScroll + > + + {(list, search) => ( +
+ {search} +
+ {i18n.OPTIONS(options.length)} +
+ + {list} +
+ )} +
+
+
+ ); +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/service_provider_filter.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/service_provider_filter.tsx new file mode 100644 index 0000000000000..3d7f9568428ef --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/service_provider_filter.tsx @@ -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) => void; +} + +const options = Object.entries(SERVICE_PROVIDERS).map(([key, { name }]) => ({ + key, + label: name, +})); + +export const ServiceProviderFilter: React.FC = ({ optionKeys, onChange }) => { + const filterId: string = 'provider'; + const onSystemFilterChange = (newOptions: MultiSelectFilterOption[]) => { + onChange({ + [filterId]: newOptions + .filter((option) => option.checked === 'on') + .map((option) => option.key), + }); + }; + + return ( + option.label} + selectedOptionKeys={optionKeys} + /> + ); +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/task_type_filter.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/task_type_filter.tsx new file mode 100644 index 0000000000000..389757b833db6 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/task_type_filter.tsx @@ -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) => void; +} + +const options = Object.values(TaskTypes).map((option) => ({ + key: option, + label: option, +})); + +export const TaskTypeFilter: React.FC = ({ optionKeys, onChange }) => { + const filterId: string = 'type'; + const onSystemFilterChange = (newOptions: MultiSelectFilterOption[]) => { + onChange({ + [filterId]: newOptions + .filter((option) => option.checked === 'on') + .map((option) => option.key), + }); + }; + + return ( + option.label} + selectedOptionKeys={optionKeys} + /> + ); +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/translations.ts b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/translations.ts new file mode 100644 index 0000000000000..f373e8f5d5a46 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/filter/translations.ts @@ -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 }, + }); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/copy_id/use_copy_id_action.test.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/copy_id/use_copy_id_action.test.tsx new file mode 100644 index 0000000000000..1445e0c41c574 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/copy_id/use_copy_id_action.test.tsx @@ -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
{action}
; + }; + + const { getByTestId } = render(); + const labelElement = getByTestId('inference-endpoints-action-copy-id-label'); + + expect(labelElement).toHaveTextContent('Copy endpoint ID'); + }); +}); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/copy_id/use_copy_id_action.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/copy_id/use_copy_id_action.tsx new file mode 100644 index 0000000000000..b43b308af068a --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/copy_id/use_copy_id_action.tsx @@ -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 ( + + {(copy) => ( + } + onClick={() => { + copy(); + onActionSuccess(); + toasts?.addSuccess({ title: i18n.COPY_ID_ACTION_SUCCESS }); + }} + size="s" + > + {i18n.COPY_ID_ACTION_LABEL} + + )} + + ); + }; + + return { getAction }; +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/index.test.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/index.test.tsx new file mode 100644 index 0000000000000..e6f4594e2d0cd --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/index.test.tsx @@ -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(); + }); + + 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); + }); +}); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/index.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/index.tsx new file mode 100644 index 0000000000000..e650192a66dc7 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/index.tsx @@ -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 = ({ + onCancel, + onConfirm, +}) => { + return ( + + {i18n.CONFIRM_DELETE_WARNING} + + ); +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/translations.ts b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/translations.ts new file mode 100644 index 0000000000000..6b2dff7d8b8f1 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/confirm_delete_endpoint/translations.ts @@ -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.', + } +); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/use_delete_action.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/use_delete_action.tsx new file mode 100644 index 0000000000000..1a2e5cdb5b51f --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/delete/use_delete_action.tsx @@ -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(false); + const [endpointToBeDeleted, setEndpointToBeDeleted] = useState(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 ( + } + onClick={() => openModal(selectedEndpoint)} + > + {i18n.DELETE_ACTION_LABEL} + + ); + }; + + return { getAction, isModalVisible, onConfirmDeletion, onCloseModal }; +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/types.ts b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/types.ts new file mode 100644 index 0000000000000..a80ccae703b7a --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/actions/types.ts @@ -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; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/use_actions.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/use_actions.tsx new file mode 100644 index 0000000000000..f76d147c68646 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_actions/use_actions.tsx @@ -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 ( + <> + + } + isOpen={isPopoverOpen} + closePopover={closePopover} + panelPaddingSize="none" + anchorPosition="downLeft" + > + + + {deleteAction.isModalVisible ? ( + + ) : null} + + ); +}; + +interface UseBulkActionsReturnValue { + actions: EuiTableComputedColumnType; +} + +export const useActions = (): UseBulkActionsReturnValue => { + return { + actions: { + align: 'right', + render: (interfaceEndpoint: InferenceEndpointUI) => { + return ; + }, + width: '165px', + }, + }; +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_deployment_status/deployment_status.test.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_deployment_status/deployment_status.test.tsx new file mode 100644 index 0000000000000..59414cd6c90ef --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_deployment_status/deployment_status.test.tsx @@ -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(); + const healthComponent = screen.getByTestId(`table-column-deployment-${status}`); + expect(healthComponent).toBeInTheDocument(); + } + ); + + it('does not render when status is notApplicable', () => { + const { container } = render(); + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_deployment_status/deployment_status.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_deployment_status/deployment_status.tsx new file mode 100644 index 0000000000000..61e6e620d2485 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_deployment_status/deployment_status.tsx @@ -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 = ({ 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 ( + + + + ); +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_deployment_status/translations.ts b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_deployment_status/translations.ts new file mode 100644 index 0000000000000..9a5448f5bf0d2 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_deployment_status/translations.ts @@ -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', + } +); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_endpoint/endpoint_info.test.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_endpoint/endpoint_info.test.tsx new file mode 100644 index 0000000000000..c92c01240425c --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_endpoint/endpoint_info.test.tsx @@ -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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + 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(); + + expect(screen.getByText('model-abc')).toBeInTheDocument(); + expect(screen.queryByText('Rate limit:')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_endpoint/endpoint_info.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_endpoint/endpoint_info.tsx new file mode 100644 index 0000000000000..ae482b609346f --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_endpoint/endpoint_info.tsx @@ -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 = ({ endpoint }) => { + return ( + + + {endpoint.model_id} + + + + + + ); +}; + +export const EndpointModelInfo: React.FC = ({ endpoint }) => { + const serviceSettings = endpoint.service_settings; + const modelId = + 'model_id' in serviceSettings + ? serviceSettings.model_id + : 'model' in serviceSettings + ? serviceSettings.model + : undefined; + + return ( + + {modelId && ( + + + + )} + + + {endpointModelAtrributes(endpoint)} + + + + ); +}; + +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}`; +} diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_endpoint/model_badge.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_endpoint/model_badge.tsx new file mode 100644 index 0000000000000..e4b241abd8199 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_endpoint/model_badge.tsx @@ -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 = ({ model }) => { + const { euiTheme } = useEuiTheme(); + + if (!model) return null; + + return {model}; +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_endpoint/translations.ts b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_endpoint/translations.ts new file mode 100644 index 0000000000000..52705999e8b44 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_endpoint/translations.ts @@ -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 }, + }); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_service_provider/service_provider.test.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_service_provider/service_provider.test.tsx new file mode 100644 index 0000000000000..a592569abb0aa --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_service_provider/service_provider.test.tsx @@ -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(); + 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(); + expect(screen.getByText('OpenAI')).toBeInTheDocument(); + const icon = screen.getByTestId('table-column-service-provider-openai'); + expect(icon).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_service_provider/service_provider.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_service_provider/service_provider.tsx new file mode 100644 index 0000000000000..c4f09213158ce --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_service_provider/service_provider.tsx @@ -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.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 = ({ providerKey }) => { + const provider = SERVICE_PROVIDERS[providerKey]; + + return provider ? ( + <> + + {provider.name} + + ) : ( + {providerKey} + ); +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_task_type/task_type.test.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_task_type/task_type.test.tsx new file mode 100644 index 0000000000000..c81ce80a619e5 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_task_type/task_type.test.tsx @@ -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(); + 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(); + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_task_type/task_type.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_task_type/task_type.tsx new file mode 100644 index 0000000000000..c294255961748 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/render_task_type/task_type.tsx @@ -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 = ({ type }) => { + if (type != null) { + return ( + + {type} + + ); + } + + return null; +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/table_columns.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/table_columns.tsx new file mode 100644 index 0000000000000..caca4449e02fa --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/render_table_columns/table_columns.tsx @@ -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 ; + } + + return null; + }, + width: '64px', + align: deploymentAlignment, + }, + { + field: 'endpoint', + name: i18n.ENDPOINT, + render: (endpoint: InferenceAPIConfigResponse) => { + if (endpoint != null) { + return ; + } + + return null; + }, + sortable: true, + }, + { + field: 'provider', + name: i18n.SERVICE_PROVIDER, + render: (provider: ServiceProviderKeys) => { + if (provider != null) { + return ; + } + + return null; + }, + sortable: false, + width: '265px', + }, + { + field: 'type', + name: i18n.TASK_TYPE, + render: (type: TaskTypes) => { + if (type != null) { + return ; + } + + return null; + }, + sortable: false, + width: '265px', + }, + actions, + ]; + + return TABLE_COLUMNS; +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/search/table_search.test.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/search/table_search.test.tsx new file mode 100644 index 0000000000000..b7d1dbcacfd71 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/search/table_search.test.tsx @@ -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(); + expect(screen.getByRole('searchbox')).toBeInTheDocument(); + }); + + it('input value matches searchKey prop', () => { + render(); + expect(screen.getByRole('searchbox')).toHaveValue('test'); + }); + + it('calls setSearchKey on input change', () => { + render(); + fireEvent.change(screen.getByRole('searchbox'), { target: { value: 'new search' } }); + expect(mockSetSearchKey).toHaveBeenCalledWith('new search'); + }); +}); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/search/table_search.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/search/table_search.tsx new file mode 100644 index 0000000000000..086dcb9656d83 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/search/table_search.tsx @@ -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>; +} + +export const TableSearch: React.FC = ({ searchKey, setSearchKey }) => { + const onSearch = useCallback( + (newSearch) => { + const trimSearch = newSearch.trim(); + setSearchKey(trimSearch); + }, + [setSearchKey] + ); + + return ( + setSearchKey(e.target.value)} + onSearch={onSearch} + value={searchKey} + /> + ); +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/table_columns.ts b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/table_columns.ts deleted file mode 100644 index 39e957f908684..0000000000000 --- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/table_columns.ts +++ /dev/null @@ -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', - }, -]; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.test.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.test.tsx index 091ff8270dd63..91a2ea959fdec 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.test.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.test.tsx @@ -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(); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.tsx index 2fb84dc4b99de..ec19ad49ad477 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.tsx @@ -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 = ({ inferenceEndpoints }) => { - const { queryParams, setQueryParams } = useAllInferenceEndpointsState(); + const [searchKey, setSearchKey] = React.useState(''); + const [deploymentStatus, setDeploymentStatus] = React.useState< + Record + >({}); + const { queryParams, setQueryParams, filterOptions, setFilterOptions } = + useAllInferenceEndpointsState(); + + const { + services: { ml, notifications }, + } = useKibana(); + + const onFilterChangedCallback = useCallback( + (newFilterOptions: Partial) => { + 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 + ); + 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 = ({ inferenceEndpoints }) ); return ( - + + + + + + + + + + + + + + + + + + ); }; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/types.ts b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/types.ts index 4afba1dda110a..4a83ac401c89a 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/types.ts +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/types.ts @@ -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; } diff --git a/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/add_empty_prompt.tsx b/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/add_empty_prompt.tsx index 69d74724016d6..ee858f7a8b640 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/add_empty_prompt.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/add_empty_prompt.tsx @@ -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 = ({ setIsInferenceFlyoutVisible }) => { return ( - } title={

{i18n.INFERENCE_ENDPOINT_LABEL}

} body={ @@ -60,20 +62,41 @@ export const AddEmptyPrompt: React.FC = ({ setIsInferenceFl {i18n.START_WITH_PREPARED_ENDPOINTS_LABEL} - + - + setIsInferenceFlyoutVisible(true)} + > + {i18n.ADD_ENDPOINT_LABEL} + + } + /> - + setIsInferenceFlyoutVisible(true)} + > + {i18n.ADD_ENDPOINT_LABEL} + + } + /> } - color="plain" - hasBorder - icon={} /> ); }; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/elser_prompt.tsx b/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/elser_prompt.tsx deleted file mode 100644 index db38b649fd66e..0000000000000 --- a/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/elser_prompt.tsx +++ /dev/null @@ -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 = ({ setIsInferenceFlyoutVisible }) => ( - setIsInferenceFlyoutVisible(true)}> - {i18n.ADD_ENDPOINT_LABEL} - - } - /> -); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/multilingual_e5_prompt.tsx b/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/endpoint_prompt.tsx similarity index 51% rename from x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/multilingual_e5_prompt.tsx rename to x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/endpoint_prompt.tsx index 69133909efcb2..b10812ef2e6a3 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/multilingual_e5_prompt.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/endpoint_prompt.tsx @@ -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 = ({ +export const EndpointPrompt: React.FC = ({ setIsInferenceFlyoutVisible, + title, + description, + footer, }) => ( setIsInferenceFlyoutVisible(true)}> - {i18n.ADD_ENDPOINT_LABEL} - - } + title={title} + titleSize="xs" + description={description} + footer={footer} /> ); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/inference_endpoints_header.tsx b/x-pack/plugins/search_inference_endpoints/public/components/inference_endpoints_header.tsx index 8b551af28bd4c..6956e470a9b77 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/inference_endpoints_header.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/components/inference_endpoints_header.tsx @@ -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 = data-test-subj="allInferenceEndpointsPage" pageTitle={i18n.INFERENCE_ENDPOINT_LABEL} description={i18n.MANAGE_INFERENCE_ENDPOINTS_LABEL} + bottomBorder={true} rightSideItems={[ - - - setIsInferenceFlyoutVisible(true)} - > - {i18n.ADD_ENDPOINT_LABEL} - - - , + setIsInferenceFlyoutVisible(true)} + > + {i18n.ADD_ENDPOINT_LABEL} + , ]} /> ); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/inference_flyout_wrapper_component.tsx b/x-pack/plugins/search_inference_endpoints/public/components/inference_flyout_wrapper_component.tsx index bf1343fe3db8b..43dc7d7d1751c 100644 --- a/x-pack/plugins/search_inference_endpoints/public/components/inference_flyout_wrapper_component.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/components/inference_flyout_wrapper_component.tsx @@ -47,9 +47,11 @@ export const InferenceFlyoutWrapperComponent: React.FC { @@ -87,17 +93,26 @@ export const InferenceFlyoutWrapperComponent: React.FC { 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(() => { diff --git a/x-pack/plugins/search_inference_endpoints/public/hooks/translations.ts b/x-pack/plugins/search_inference_endpoints/public/hooks/translations.ts new file mode 100644 index 0000000000000..ee1dc26a54817 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/hooks/translations.ts @@ -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.', + } +); diff --git a/x-pack/plugins/search_inference_endpoints/public/hooks/use_all_inference_endpoints_state.tsx b/x-pack/plugins/search_inference_endpoints/public/hooks/use_all_inference_endpoints_state.tsx index 4979cdf7994fc..c2a0486578a9e 100644 --- a/x-pack/plugins/search_inference_endpoints/public/hooks/use_all_inference_endpoints_state.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/hooks/use_all_inference_endpoints_state.tsx @@ -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) => void; + filterOptions: FilterOptions; + setFilterOptions: (filterOptions: Partial) => void; } export function useAllInferenceEndpointsState(): UseAllInferenceEndpointsStateReturn { - const [tableState, setTableState] = useState( + const [tableState, setTableState] = useState( 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) => { setState({ + filterOptions: tableState.filterOptions, queryParams: { ...tableState.queryParams, ...newQueryParams }, }); }, + filterOptions: { + ...DEFAULT_INFERENCE_ENDPOINTS_TABLE_STATE.filterOptions, + ...tableState.filterOptions, + }, + setFilterOptions: (newFilterOptions: Partial) => { + setState({ + filterOptions: { ...tableState.filterOptions, ...newFilterOptions }, + queryParams: tableState.queryParams, + }); + }, }; } diff --git a/x-pack/plugins/search_inference_endpoints/public/hooks/use_delete_endpoint.test.tsx b/x-pack/plugins/search_inference_endpoints/public/hooks/use_delete_endpoint.test.tsx new file mode 100644 index 0000000000000..72e8dfe8418e2 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/hooks/use_delete_endpoint.test.tsx @@ -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 {children}; + }; + + 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, + }); + }); +}); diff --git a/x-pack/plugins/search_inference_endpoints/public/hooks/use_delete_endpoint.tsx b/x-pack/plugins/search_inference_endpoints/public/hooks/use_delete_endpoint.tsx new file mode 100644 index 0000000000000..e5464703dcfe2 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/hooks/use_delete_endpoint.tsx @@ -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, + }); + }, + } + ); +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/hooks/use_table_data.test.tsx b/x-pack/plugins/search_inference_endpoints/public/hooks/use_table_data.test.tsx index e0026cbffff98..a8d0326a4c36f 100644 --- a/x-pack/plugins/search_inference_endpoints/public/hooks/use_table_data.test.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/hooks/use_table_data.test.tsx @@ -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'); + }); }); diff --git a/x-pack/plugins/search_inference_endpoints/public/hooks/use_table_data.tsx b/x-pack/plugins/search_inference_endpoints/public/hooks/use_table_data.tsx index 304c469e06093..54d865aad5190 100644 --- a/x-pack/plugins/search_inference_endpoints/public/hooks/use_table_data.tsx +++ b/x-pack/plugins/search_inference_endpoints/public/hooks/use_table_data.tsx @@ -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 ): 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]); diff --git a/x-pack/plugins/search_inference_endpoints/server/lib/delete_inference_endpoint.test.ts b/x-pack/plugins/search_inference_endpoints/server/lib/delete_inference_endpoint.test.ts new file mode 100644 index 0000000000000..f6c9a5625230d --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/server/lib/delete_inference_endpoint.test.ts @@ -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}`, + }); + }); +}); diff --git a/x-pack/plugins/search_inference_endpoints/server/lib/delete_inference_endpoint.ts b/x-pack/plugins/search_inference_endpoints/server/lib/delete_inference_endpoint.ts new file mode 100644 index 0000000000000..c294820e4943e --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/server/lib/delete_inference_endpoint.ts @@ -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 => { + return await client.transport.request({ + method: 'DELETE', + path: `/_inference/${type}/${id}`, + }); +}; diff --git a/x-pack/plugins/search_inference_endpoints/server/routes.ts b/x-pack/plugins/search_inference_endpoints/server/routes.ts index d5f010b902c52..7456888aabb19 100644 --- a/x-pack/plugins/search_inference_endpoints/server/routes.ts +++ b/x-pack/plugins/search_inference_endpoints/server/routes.ts @@ -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(); + }) + ); }