diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5f2e9ed4992d4..4d7cc07659dde 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -717,6 +717,7 @@ x-pack/plugins/search_connectors @elastic/search-kibana packages/kbn-search-errors @elastic/kibana-data-discovery examples/search_examples @elastic/kibana-data-discovery packages/kbn-search-index-documents @elastic/search-kibana +x-pack/plugins/search_inference_endpoints @elastic/search-kibana x-pack/plugins/search_notebooks @elastic/search-kibana x-pack/plugins/search_playground @elastic/search-kibana packages/kbn-search-response-warnings @elastic/kibana-data-discovery diff --git a/config/serverless.es.yml b/config/serverless.es.yml index 0a609b0d40b2c..531fa5520c0ae 100644 --- a/config/serverless.es.yml +++ b/config/serverless.es.yml @@ -64,5 +64,8 @@ data_visualizer.resultLinks.fileBeat.enabled: false # Search Playground xpack.searchPlayground.ui.enabled: true +# Search InferenceEndpoints +xpack.searchInferenceEndpoints.ui.enabled: false + # Search Notebooks xpack.search.notebooks.catalog.url: https://elastic-enterprise-search.s3.us-east-2.amazonaws.com/serverless/catalog.json diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 8bd8e4b00040e..f14a0de00211f 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -785,6 +785,10 @@ It uses Chromium and Puppeteer underneath to run the browser in headless mode. |This plugin contains common assets and endpoints for the use of connectors in Kibana. Primarily used by the enterprise_search and serverless_search plugins. +|{kib-repo}blob/{branch}/x-pack/plugins/search_inference_endpoints/README.md[searchInferenceEndpoints] +|The Inference Endpoints is a tool used to manage inference endpoints + + |{kib-repo}blob/{branch}/x-pack/plugins/search_notebooks/README.mdx[searchNotebooks] |This plugin contains endpoints and components for rendering search python notebooks in the persistent dev console. diff --git a/package.json b/package.json index ccf6429b22738..b142765c25ed8 100644 --- a/package.json +++ b/package.json @@ -728,6 +728,7 @@ "@kbn/search-errors": "link:packages/kbn-search-errors", "@kbn/search-examples-plugin": "link:examples/search_examples", "@kbn/search-index-documents": "link:packages/kbn-search-index-documents", + "@kbn/search-inference-endpoints": "link:x-pack/plugins/search_inference_endpoints", "@kbn/search-notebooks": "link:x-pack/plugins/search_notebooks", "@kbn/search-playground": "link:x-pack/plugins/search_playground", "@kbn/search-response-warnings": "link:packages/kbn-search-response-warnings", diff --git a/packages/deeplinks/search/constants.ts b/packages/deeplinks/search/constants.ts index fb8a0c459cbd0..36d31d22dfe21 100644 --- a/packages/deeplinks/search/constants.ts +++ b/packages/deeplinks/search/constants.ts @@ -8,6 +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_APPLICATIONS_APP_ID = 'enterpriseSearchApplications'; export const ENTERPRISE_SEARCH_ANALYTICS_APP_ID = 'enterpriseSearchAnalytics'; export const ENTERPRISE_SEARCH_APPSEARCH_APP_ID = 'appSearch'; @@ -15,3 +16,4 @@ export const ENTERPRISE_SEARCH_WORKPLACESEARCH_APP_ID = 'workplaceSearch'; export const SERVERLESS_ES_APP_ID = 'serverlessElasticsearch'; export const SERVERLESS_ES_CONNECTORS_ID = 'serverlessConnectors'; export const SERVERLESS_ES_SEARCH_PLAYGROUND_ID = 'searchPlayground'; +export const SERVERLESS_ES_SEARCH_INFERENCE_ENDPOINTS_ID = 'searchInferenceEndpoints'; diff --git a/packages/deeplinks/search/deep_links.ts b/packages/deeplinks/search/deep_links.ts index 352640e6d4947..8eeceff8f8ca2 100644 --- a/packages/deeplinks/search/deep_links.ts +++ b/packages/deeplinks/search/deep_links.ts @@ -16,6 +16,7 @@ import { ENTERPRISE_SEARCH_APPSEARCH_APP_ID, ENTERPRISE_SEARCH_WORKPLACESEARCH_APP_ID, SERVERLESS_ES_SEARCH_PLAYGROUND_ID, + SERVERLESS_ES_SEARCH_INFERENCE_ENDPOINTS_ID, } from './constants'; export type EnterpriseSearchApp = typeof ENTERPRISE_SEARCH_APP_ID; @@ -27,6 +28,7 @@ export type EnterpriseSearchWorkplaceSearchApp = typeof ENTERPRISE_SEARCH_WORKPL export type ServerlessSearchApp = typeof SERVERLESS_ES_APP_ID; export type ConnectorsId = typeof SERVERLESS_ES_CONNECTORS_ID; export type SearchPlaygroundId = typeof SERVERLESS_ES_SEARCH_PLAYGROUND_ID; +export type SearchInferenceEndpointsId = typeof SERVERLESS_ES_SEARCH_INFERENCE_ENDPOINTS_ID; export type ContentLinkId = 'searchIndices' | 'connectors' | 'webCrawlers'; @@ -44,6 +46,7 @@ export type DeepLinkId = | ServerlessSearchApp | ConnectorsId | SearchPlaygroundId + | SearchInferenceEndpointsId | `${EnterpriseSearchContentApp}:${ContentLinkId}` | `${EnterpriseSearchApplicationsApp}:${ApplicationsLinkId}` | `${EnterpriseSearchAppsearchApp}:${AppsearchLinkId}`; diff --git a/packages/deeplinks/search/index.ts b/packages/deeplinks/search/index.ts index ba15b8c4890c3..663d625e9fd72 100644 --- a/packages/deeplinks/search/index.ts +++ b/packages/deeplinks/search/index.ts @@ -9,6 +9,7 @@ export { ENTERPRISE_SEARCH_APP_ID, ENTERPRISE_SEARCH_CONTENT_APP_ID, + ENTERPRISE_SEARCH_INFERENCE_ENDPOINTS_APP_ID, ENTERPRISE_SEARCH_APPLICATIONS_APP_ID, ENTERPRISE_SEARCH_ANALYTICS_APP_ID, ENTERPRISE_SEARCH_APPSEARCH_APP_ID, diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index a7ec0e03f3dda..336cfd2c6b93d 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -31,7 +31,7 @@ pageLoadAssetSize: dataQuality: 19384 datasetQuality: 50624 dataViewEditor: 28082 - dataViewFieldEditor: 27000 + dataViewFieldEditor: 42021 dataViewManagement: 5300 dataViews: 65000 dataVisualizer: 27530 @@ -41,8 +41,8 @@ pageLoadAssetSize: discoverShared: 17111 embeddable: 87309 embeddableEnhanced: 22107 - enterpriseSearch: 50858 - esqlDataGrid: 24598 + enterpriseSearch: 66810 + esqlDataGrid: 24582 esUiShared: 326654 eventAnnotation: 30000 eventAnnotationListing: 25841 @@ -132,6 +132,7 @@ pageLoadAssetSize: screenshotMode: 17856 screenshotting: 22870 searchConnectors: 30000 + searchInferenceEndpoints: 20470 searchNotebooks: 18942 searchPlayground: 19325 searchprofiler: 67080 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 0e12abb64bcc7..4ffe9d483a1a4 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,6 +135,7 @@ export const applicationUsageSchema = { canvas: commonSchema, enterpriseSearch: commonSchema, enterpriseSearchContent: commonSchema, + enterpriseSearchInferenceEndpoints: 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 417a59ad71bf7..1e2c942ba2a5c 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -2098,6 +2098,137 @@ } } }, + "enterpriseSearchInferenceEndpoints": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "Always `main`" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen over the last 90 days" + } + }, + "views": { + "type": "array", + "items": { + "properties": { + "appId": { + "type": "keyword", + "_meta": { + "description": "The application being tracked" + } + }, + "viewId": { + "type": "keyword", + "_meta": { + "description": "The application view being tracked" + } + }, + "clicks_total": { + "type": "long", + "_meta": { + "description": "General number of clicks in the application sub view since we started counting them" + } + }, + "clicks_7_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 7 days" + } + }, + "clicks_30_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 30 days" + } + }, + "clicks_90_days": { + "type": "long", + "_meta": { + "description": "General number of clicks in the active application sub view over the last 90 days" + } + }, + "minutes_on_screen_total": { + "type": "float", + "_meta": { + "description": "Minutes the application sub view is active and on-screen since we started counting them." + } + }, + "minutes_on_screen_7_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 7 days" + } + }, + "minutes_on_screen_30_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 30 days" + } + }, + "minutes_on_screen_90_days": { + "type": "float", + "_meta": { + "description": "Minutes the application is active and on-screen active application sub view over the last 90 days" + } + } + } + } + } + } + }, "enterpriseSearchAnalytics": { "properties": { "appId": { diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 7ccbc90d8760e..31e0a44b9e823 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -314,6 +314,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.rollup.ui.enabled (boolean)', 'xpack.saved_object_tagging.cache_refresh_interval (duration)', 'xpack.searchPlayground.ui.enabled (boolean)', + 'xpack.searchInferenceEndpoints.ui.enabled (boolean)', 'xpack.security.loginAssistanceMessage (string)', 'xpack.security.sameSiteCookies (alternatives)', 'xpack.security.showInsecureClusterWarning (boolean)', diff --git a/tsconfig.base.json b/tsconfig.base.json index cd3deff3a294e..da0daf44cd411 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1428,6 +1428,8 @@ "@kbn/search-examples-plugin/*": ["examples/search_examples/*"], "@kbn/search-index-documents": ["packages/kbn-search-index-documents"], "@kbn/search-index-documents/*": ["packages/kbn-search-index-documents/*"], + "@kbn/search-inference-endpoints": ["x-pack/plugins/search_inference_endpoints"], + "@kbn/search-inference-endpoints/*": ["x-pack/plugins/search_inference_endpoints/*"], "@kbn/search-notebooks": ["x-pack/plugins/search_notebooks"], "@kbn/search-notebooks/*": ["x-pack/plugins/search_notebooks/*"], "@kbn/search-playground": ["x-pack/plugins/search_playground"], diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index 8059502946861..2943af3cf46d5 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -94,6 +94,7 @@ "xpack.screenshotting": "plugins/screenshotting", "xpack.searchNotebooks": "plugins/search_notebooks", "xpack.searchPlayground": "plugins/search_playground", + "xpack.searchInferenceEndpoints": "plugins/search_inference_endpoints", "xpack.searchProfiler": "plugins/searchprofiler", "xpack.security": "plugins/security", "xpack.server": "legacy/server", diff --git a/x-pack/plugins/enterprise_search/common/constants.ts b/x-pack/plugins/enterprise_search/common/constants.ts index 8f49e3cae3c02..9830f66a441ed 100644 --- a/x-pack/plugins/enterprise_search/common/constants.ts +++ b/x-pack/plugins/enterprise_search/common/constants.ts @@ -8,6 +8,7 @@ import { ENTERPRISE_SEARCH_APP_ID, ENTERPRISE_SEARCH_CONTENT_APP_ID, + ENTERPRISE_SEARCH_INFERENCE_ENDPOINTS_APP_ID, ENTERPRISE_SEARCH_APPLICATIONS_APP_ID, ENTERPRISE_SEARCH_ANALYTICS_APP_ID, ENTERPRISE_SEARCH_APPSEARCH_APP_ID, @@ -176,6 +177,22 @@ export const VECTOR_SEARCH_PLUGIN = { URL: '/app/enterprise_search/vector_search', }; +export const INFERENCE_ENDPOINTS_PLUGIN = { + ID: ENTERPRISE_SEARCH_INFERENCE_ENDPOINTS_APP_ID, + NAME: i18n.translate('xpack.enterpriseSearch.inferenceEndpoints.productName', { + defaultMessage: 'Inference Endpoints', + }), + NAV_TITLE: i18n.translate('xpack.enterpriseSearch.inferenceEndpoints.navTitle', { + defaultMessage: 'Relevance', + }), + DESCRIPTION: i18n.translate('xpack.enterpriseSearch.inferenceEndpoints.description', { + defaultMessage: 'View for managing inference endpoints.', + }), + URL: '/app/enterprise_search/relevance', + LOGO: 'logoEnterpriseSearch', + SUPPORT_URL: 'https://discuss.elastic.co/c/enterprise-search/', +}; + export const LICENSED_SUPPORT_URL = 'https://support.elastic.co'; export const JSON_HEADER = { diff --git a/x-pack/plugins/enterprise_search/kibana.jsonc b/x-pack/plugins/enterprise_search/kibana.jsonc index cefa247025aa5..3855fd3d9e1f9 100644 --- a/x-pack/plugins/enterprise_search/kibana.jsonc +++ b/x-pack/plugins/enterprise_search/kibana.jsonc @@ -31,6 +31,7 @@ "console", "searchConnectors", "searchPlayground", + "searchInferenceEndpoints", "embeddable", "discover", "charts", diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts index 5aa7f5236e269..cca5523ded681 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea_logic/kibana_logic.mock.ts @@ -64,6 +64,7 @@ export const mockKibanaValues = { hasWebCrawler: true, }, renderHeaderActions: jest.fn(), + searchInferenceEndpoints: null, searchPlayground: searchPlaygroundMock.createStart(), security: securityMock.createStart(), setBreadcrumbs: jest.fn(), diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/components/inference_endpoints.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/components/inference_endpoints.tsx new file mode 100644 index 0000000000000..440638b57d4b6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/components/inference_endpoints.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 { useValues } from 'kea'; + +import { i18n } from '@kbn/i18n'; + +import { KibanaLogic } from '../../shared/kibana'; + +import { EnterpriseSearchRelevancePageTemplate } from './layout/page_template'; + +export const InferenceEndpoints: React.FC = () => { + const { searchInferenceEndpoints } = useValues(KibanaLogic); + + if (!searchInferenceEndpoints) { + return null; + } + return ( + + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/components/layout/index.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/components/layout/index.ts new file mode 100644 index 0000000000000..0a155ce20b555 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/components/layout/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { EnterpriseSearchRelevancePageTemplate } from './page_template'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/components/layout/page_template.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/components/layout/page_template.test.tsx new file mode 100644 index 0000000000000..6b9e6b891a59f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/components/layout/page_template.test.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. + */ + +jest.mock('../../../shared/layout/nav', () => ({ + useEnterpriseSearchNav: () => [], +})); + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { i18n } from '@kbn/i18n'; + +import { SetEnterpriseSearchRelevanceChrome } from '../../../shared/kibana_chrome'; +import { EnterpriseSearchPageTemplateWrapper } from '../../../shared/layout'; +import { SendEnterpriseSearchTelemetry } from '../../../shared/telemetry'; + +import { EnterpriseSearchRelevancePageTemplate } from './page_template'; + +describe('EnterpriseSearchRelevancePageTemplate', () => { + it('renders', () => { + const wrapper = shallow( + +
+ {i18n.translate('xpack.enterpriseSearch..div.worldLabel', { defaultMessage: 'world' })} +
+
+ ); + + expect(wrapper.type()).toEqual(EnterpriseSearchPageTemplateWrapper); + expect(wrapper.prop('solutionNav')).toEqual({ items: [], name: 'Search' }); + expect(wrapper.find('.hello').text()).toEqual('world'); + }); + + describe('page chrome', () => { + it('takes a breadcrumb array & renders a product-specific page chrome', () => { + const wrapper = shallow(); + const setPageChrome = wrapper + .find(EnterpriseSearchPageTemplateWrapper) + .prop('setPageChrome') as any; + + expect(setPageChrome.type).toEqual(SetEnterpriseSearchRelevanceChrome); + expect(setPageChrome.props.trail).toEqual(['Some page']); + }); + }); + + describe('page telemetry', () => { + it('takes a metric & renders product-specific telemetry viewed event', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(SendEnterpriseSearchTelemetry).prop('action')).toEqual('viewed'); + expect(wrapper.find(SendEnterpriseSearchTelemetry).prop('metric')).toEqual('some_page'); + }); + }); + + describe('props', () => { + it('passes down any ...pageTemplateProps that EnterpriseSearchPageTemplateWrapper accepts', () => { + const wrapper = shallow( + } + /> + ); + + expect( + wrapper.find(EnterpriseSearchPageTemplateWrapper).prop('pageHeader')!.pageTitle + ).toEqual('hello world'); + expect(wrapper.find(EnterpriseSearchPageTemplateWrapper).prop('isLoading')).toEqual(false); + expect(wrapper.find(EnterpriseSearchPageTemplateWrapper).prop('emptyState')).toEqual(
); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/components/layout/page_template.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/components/layout/page_template.tsx new file mode 100644 index 0000000000000..258b9c9a68ae1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/components/layout/page_template.tsx @@ -0,0 +1,37 @@ +/* + * 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 { SEARCH_PRODUCT_NAME } from '../../../../../common/constants'; +import { SetEnterpriseSearchRelevanceChrome } from '../../../shared/kibana_chrome'; +import { EnterpriseSearchPageTemplateWrapper, PageTemplateProps } from '../../../shared/layout'; +import { useEnterpriseSearchNav } from '../../../shared/layout'; +import { SendEnterpriseSearchTelemetry } from '../../../shared/telemetry'; + +export const EnterpriseSearchRelevancePageTemplate: React.FC = ({ + children, + pageChrome, + pageViewTelemetry, + ...pageTemplateProps +}) => { + return ( + } + > + {pageViewTelemetry && ( + + )} + {children} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/components/not_found/index.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/components/not_found/index.ts new file mode 100644 index 0000000000000..482c1a58faa9c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/components/not_found/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { NotFound } from './not_found'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/components/not_found/not_found.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/components/not_found/not_found.test.tsx new file mode 100644 index 0000000000000..7b5436958edb7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/components/not_found/not_found.test.tsx @@ -0,0 +1,38 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { NotFoundPrompt } from '../../../shared/not_found'; +import { SendEnterpriseSearchTelemetry } from '../../../shared/telemetry'; +import { EnterpriseSearchRelevancePageTemplate } from '../layout'; + +import { NotFound } from '.'; + +describe('NotFound', () => { + const wrapper = shallow(); + + it('renders the shared not found prompt', () => { + expect(wrapper.find(NotFoundPrompt)).toHaveLength(1); + }); + + it('renders a telemetry error event', () => { + expect(wrapper.find(SendEnterpriseSearchTelemetry).prop('action')).toEqual('error'); + }); + + it('passes optional preceding page chrome', () => { + wrapper.setProps({ pageChrome: ['Inference Endpoints', 'some-index'] }); + + expect(wrapper.find(EnterpriseSearchRelevancePageTemplate).prop('pageChrome')).toEqual([ + 'Inference Endpoints', + 'some-index', + '404', + ]); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/components/not_found/not_found.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/components/not_found/not_found.tsx new file mode 100644 index 0000000000000..1c599fdc884e3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/components/not_found/not_found.tsx @@ -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 React from 'react'; + +import { INFERENCE_ENDPOINTS_PLUGIN } from '../../../../../common/constants'; +import { PageTemplateProps } from '../../../shared/layout'; +import { NotFoundPrompt } from '../../../shared/not_found'; +import { SendEnterpriseSearchTelemetry } from '../../../shared/telemetry'; +import { EnterpriseSearchRelevancePageTemplate } from '../layout'; + +export const NotFound: React.FC = ({ pageChrome = [] }) => { + return ( + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/index.test.tsx new file mode 100644 index 0000000000000..5ac28448c7c4d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/index.test.tsx @@ -0,0 +1,46 @@ +/* + * 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 { setMockValues } from '../__mocks__/kea_logic'; +import '../__mocks__/shallow_useeffect.mock'; +import '../__mocks__/enterprise_search_url.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { VersionMismatchPage } from '../shared/version_mismatch'; + +import { EnterpriseSearchRelevance, EnterpriseSearchRelevanceConfigured } from '.'; + +describe('EnterpriseSearchRelevance', () => { + it('renders VersionMismatchPage when there are mismatching versions', () => { + setMockValues({ config: { canDeployEntSearch: true, host: 'host' } }); + const wrapper = shallow( + + ); + + expect(wrapper.find(VersionMismatchPage)).toHaveLength(1); + }); + + it('renders EnterpriseSearchRelevanceConfigured when config.host is set & available', () => { + setMockValues({ + config: { canDeployEntSearch: true, host: 'some.url' }, + errorConnectingMessage: '', + }); + const wrapper = shallow(); + + expect(wrapper.find(EnterpriseSearchRelevanceConfigured)).toHaveLength(1); + }); + + it('renders EnterpriseSearchRelevanceConfigured when config.host is not set & Ent Search cannot be deployed', () => { + setMockValues({ config: { canDeployEntSearch: false, host: '' }, errorConnectingMessage: '' }); + const wrapper = shallow(); + + expect(wrapper.find(EnterpriseSearchRelevanceConfigured)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/index.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/index.tsx new file mode 100644 index 0000000000000..6e75d8055619b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/index.tsx @@ -0,0 +1,65 @@ +/* + * 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 { Redirect } from 'react-router-dom'; + +import { useValues } from 'kea'; + +import { Route, Routes } from '@kbn/shared-ux-router'; + +import { isVersionMismatch } from '../../../common/is_version_mismatch'; +import { InitialAppData } from '../../../common/types'; +import { ErrorStatePrompt } from '../shared/error_state'; +import { HttpLogic } from '../shared/http'; +import { VersionMismatchPage } from '../shared/version_mismatch'; + +import { InferenceEndpoints } from './components/inference_endpoints'; +import { NotFound } from './components/not_found'; +import { INFERENCE_ENDPOINTS_PATH, ERROR_STATE_PATH, ROOT_PATH } from './routes'; + +export const EnterpriseSearchRelevance: React.FC = (props) => { + const { errorConnectingMessage } = useValues(HttpLogic); + const { enterpriseSearchVersion, kibanaVersion } = props; + const incompatibleVersions = isVersionMismatch(enterpriseSearchVersion, kibanaVersion); + + const showView = () => { + if (incompatibleVersions) { + return ( + + ); + } + + return )} />; + }; + + return ( + + + {errorConnectingMessage ? : } + + {showView()} + + ); +}; + +export const EnterpriseSearchRelevanceConfigured: React.FC> = () => { + return ( + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/jest.config.js b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/jest.config.js new file mode 100644 index 0000000000000..d186396d14a07 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/jest.config.js @@ -0,0 +1,28 @@ +/* + * 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../../../..', + roots: [ + '/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance', + ], + collectCoverage: true, + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/enterprise_search/public/applications/**/*.{ts,tsx}', + '!/x-pack/plugins/enterprise_search/public/*.ts', + '!/x-pack/plugins/enterprise_search/server/*.ts', + '!/x-pack/plugins/enterprise_search/public/applications/test_helpers/**/*.{ts,tsx}', + ], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance', + modulePathIgnorePatterns: [ + '/x-pack/plugins/enterprise_search/public/applications/app_search/cypress', + '/x-pack/plugins/enterprise_search/public/applications/workplace_search/cypress', + ], +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/routes.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/routes.ts new file mode 100644 index 0000000000000..5ea7ad1c781a8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_relevance/routes.ts @@ -0,0 +1,10 @@ +/* + * 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 const ROOT_PATH = '/'; +export const ERROR_STATE_PATH = '/error_state'; +export const INFERENCE_ENDPOINTS_PATH = `${ROOT_PATH}inference_endpoints`; diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index d6bc0e0da5d44..ea45e121470e2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -128,6 +128,7 @@ export const renderApp = ( HeaderActions ? renderHeaderActions.bind(null, HeaderActions, store, params) : undefined ), searchPlayground: plugins.searchPlayground, + searchInferenceEndpoints: plugins.searchInferenceEndpoints, security, setBreadcrumbs: chrome.setBreadcrumbs, setChromeIsVisible: chrome.setIsVisible, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts index f9da95c1b2667..da18e9e8bb44f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts @@ -29,6 +29,7 @@ import { LensPublicStart } from '@kbn/lens-plugin/public'; import { MlPluginStart } from '@kbn/ml-plugin/public'; import { ELASTICSEARCH_URL_PLACEHOLDER } from '@kbn/search-api-panels/constants'; import { ConnectorDefinition } from '@kbn/search-connectors-plugin/public'; +import { SearchInferenceEndpointsPluginStart } from '@kbn/search-inference-endpoints/public'; import { SearchPlaygroundPluginStart } from '@kbn/search-playground/public'; import { AuthenticatedUser, SecurityPluginStart } from '@kbn/security-plugin/public'; import { SharePluginStart } from '@kbn/share-plugin/public'; @@ -65,6 +66,7 @@ export interface KibanaLogicProps { productFeatures: ProductFeatures; renderHeaderActions(HeaderActions?: FC): void; searchPlayground?: SearchPlaygroundPluginStart; + searchInferenceEndpoints?: SearchInferenceEndpointsPluginStart; security?: SecurityPluginStart; setBreadcrumbs(crumbs: ChromeBreadcrumb[]): void; setChromeIsVisible(isVisible: boolean): void; @@ -97,6 +99,7 @@ export interface KibanaValues { productFeatures: ProductFeatures; renderHeaderActions(HeaderActions?: FC): void; searchPlayground: SearchPlaygroundPluginStart | null; + searchInferenceEndpoints: SearchInferenceEndpointsPluginStart | null; security: SecurityPluginStart | null; setBreadcrumbs(crumbs: ChromeBreadcrumb[]): void; setChromeIsVisible(isVisible: boolean): void; @@ -141,6 +144,7 @@ export const KibanaLogic = kea>({ productFeatures: [props.productFeatures, {}], renderHeaderActions: [props.renderHeaderActions, {}], searchPlayground: [props.searchPlayground || null, {}], + searchInferenceEndpoints: [props.searchInferenceEndpoints || null, {}], security: [props.security || null, {}], setBreadcrumbs: [props.setBreadcrumbs, {}], setChromeIsVisible: [props.setChromeIsVisible, {}], diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts index d30ceee39b75b..ac3e6d7a6437d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts @@ -14,6 +14,7 @@ import { ANALYTICS_PLUGIN, APP_SEARCH_PLUGIN, ENTERPRISE_SEARCH_CONTENT_PLUGIN, + INFERENCE_ENDPOINTS_PLUGIN, ENTERPRISE_SEARCH_OVERVIEW_PLUGIN, ENTERPRISE_SEARCH_PRODUCT_NAME, AI_SEARCH_PLUGIN, @@ -151,6 +152,9 @@ export const useEnterpriseSearchContentBreadcrumbs = (breadcrumbs: Breadcrumbs = ...breadcrumbs, ]); +export const useEnterpriseSearchRelevanceBreadcrumbs = (breadcrumbs: Breadcrumbs = []) => + useSearchBreadcrumbs([{ text: INFERENCE_ENDPOINTS_PLUGIN.NAV_TITLE, path: '/' }, ...breadcrumbs]); + export const useSearchExperiencesBreadcrumbs = (breadcrumbs: Breadcrumbs = []) => useSearchBreadcrumbs([{ text: SEARCH_EXPERIENCES_PLUGIN.NAV_TITLE, path: '/' }, ...breadcrumbs]); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/index.ts index 75e72ab5e0208..ae2151287b1d1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/index.ts @@ -9,6 +9,7 @@ export { SetSearchChrome, SetAnalyticsChrome, SetEnterpriseSearchContentChrome, + SetEnterpriseSearchRelevanceChrome, SetElasticsearchChrome, SetAiSearchChrome, SetAppSearchChrome, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx index 5c6758509c01d..ac85bd89b6ba9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx @@ -19,6 +19,7 @@ import { useEnterpriseSearchApplicationsBreadcrumbs, useAnalyticsBreadcrumbs, useEnterpriseSearchContentBreadcrumbs, + useEnterpriseSearchRelevanceBreadcrumbs, useAiSearchBreadcrumbs, useElasticsearchBreadcrumbs, useAppSearchBreadcrumbs, @@ -177,6 +178,19 @@ export const SetEnterpriseSearchContentChrome: React.FC = ({ tra return null; }; +export const SetEnterpriseSearchRelevanceChrome: React.FC = ({ trail = [] }) => { + const { setBreadcrumbs } = useValues(KibanaLogic); + + const crumbs = useGenerateBreadcrumbs(trail); + const breadcrumbs = useEnterpriseSearchRelevanceBreadcrumbs(crumbs); + + useEffect(() => { + setBreadcrumbs(breadcrumbs); + }, [trail]); + + return null; +}; + export const SetSearchExperiencesChrome: React.FC = ({ trail = [] }) => { const { setBreadcrumbs, setDocTitle } = useValues(KibanaLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx index 3930e0f6c36f1..f818dfb9141f3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/nav.test.tsx @@ -84,6 +84,18 @@ const baseNavItems = [ ], name: 'Build', }, + { + id: 'relevance', + items: [ + { + href: '/app/enterprise_search/relevance/inference_endpoints', + id: 'inference_endpoints', + items: undefined, + name: 'Inference Endpoints', + }, + ], + name: 'Relevance', + }, { id: 'es_getting_started', items: [ @@ -244,6 +256,7 @@ describe('useEnterpriseSearchApplicationNav', () => { expect(navItems?.slice(1).map((ni) => ni.name)).toEqual([ 'Content', 'Build', + 'Relevance', 'Getting started', 'Enterprise Search', ]); @@ -303,6 +316,7 @@ describe('useEnterpriseSearchApplicationNav', () => { expect(navItems?.slice(1).map((ni) => ni.name)).toEqual([ 'Content', 'Build', + 'Relevance', 'Getting started', 'Enterprise Search', ]); 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 c2268d254197d..bd53ed235d4cb 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 @@ -23,6 +23,7 @@ import { AI_SEARCH_PLUGIN, VECTOR_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN, + INFERENCE_ENDPOINTS_PLUGIN, } from '../../../../common/constants'; import { SEARCH_APPLICATIONS_PATH, @@ -35,6 +36,8 @@ import { CRAWLERS_PATH, SEARCH_INDICES_PATH, } from '../../enterprise_search_content/routes'; + +import { INFERENCE_ENDPOINTS_PATH } from '../../enterprise_search_relevance/routes'; import { KibanaLogic } from '../kibana'; import { generateNavLink } from './nav_link_helpers'; @@ -147,6 +150,25 @@ 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', + }), + }, { id: 'es_getting_started', items: [ diff --git a/x-pack/plugins/enterprise_search/public/navigation_tree.ts b/x-pack/plugins/enterprise_search/public/navigation_tree.ts index 9dc1d324fd118..dc079aec90688 100644 --- a/x-pack/plugins/enterprise_search/public/navigation_tree.ts +++ b/x-pack/plugins/enterprise_search/public/navigation_tree.ts @@ -206,6 +206,13 @@ export const getNavigationTreeDefinition = ({ defaultMessage: 'Build', }), }, + { + children: [{ link: 'searchInferenceEndpoints' }], + id: 'relevance', + title: i18n.translate('xpack.enterpriseSearch.searchNav.relevance', { + defaultMessage: 'Relevance', + }), + }, { children: [ { diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index 96c4bab2b8059..9c03fb623fa3d 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -32,6 +32,7 @@ import { MlPluginStart } from '@kbn/ml-plugin/public'; import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'; import { ELASTICSEARCH_URL_PLACEHOLDER } from '@kbn/search-api-panels/constants'; import { SearchConnectorsPluginStart } from '@kbn/search-connectors-plugin/public'; +import { SearchInferenceEndpointsPluginStart } from '@kbn/search-inference-endpoints/public'; import { SearchPlaygroundPluginStart } from '@kbn/search-playground/public'; import { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/public'; import { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public'; @@ -48,6 +49,7 @@ import { SEARCH_PRODUCT_NAME, VECTOR_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN, + INFERENCE_ENDPOINTS_PLUGIN, } from '../common/constants'; import { CreatIndexLocatorDefinition, @@ -63,6 +65,7 @@ import { CRAWLERS_PATH, } from './applications/enterprise_search_content/routes'; +import { INFERENCE_ENDPOINTS_PATH } from './applications/enterprise_search_relevance/routes'; import { docLinks } from './applications/shared/doc_links'; import type { DynamicSideNavItems } from './navigation_tree'; @@ -94,6 +97,7 @@ export interface PluginsStart { navigation: NavigationPublicPluginStart; searchConnectors?: SearchConnectorsPluginStart; searchPlayground?: SearchPlaygroundPluginStart; + searchInferenceEndpoints?: SearchInferenceEndpointsPluginStart; security?: SecurityPluginStart; share?: SharePluginStart; } @@ -128,6 +132,19 @@ const contentLinks: AppDeepLink[] = [ }, ]; +const relevanceLinks: AppDeepLink[] = [ + { + id: 'inferenceEndpoints', + path: `/${INFERENCE_ENDPOINTS_PATH}`, + title: i18n.translate( + 'xpack.enterpriseSearch.navigation.relevanceInferenceEndpointsLinkLabel', + { + defaultMessage: 'Inference Endpoints', + } + ), + }, +]; + const applicationsLinks: AppDeepLink[] = [ { id: 'playground', @@ -395,6 +412,31 @@ 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: [], + }); + core.application.register({ appRoute: SEARCH_EXPERIENCES_PLUGIN.URL, category: DEFAULT_APP_CATEGORIES.enterpriseSearch, diff --git a/x-pack/plugins/enterprise_search/tsconfig.json b/x-pack/plugins/enterprise_search/tsconfig.json index 53f364e26cac2..3a66b8350be4f 100644 --- a/x-pack/plugins/enterprise_search/tsconfig.json +++ b/x-pack/plugins/enterprise_search/tsconfig.json @@ -70,6 +70,7 @@ "@kbn/es-errors", "@kbn/search-connectors-plugin", "@kbn/search-playground", + "@kbn/search-inference-endpoints", "@kbn/utility-types", "@kbn/index-management", "@kbn/deeplinks-search", diff --git a/x-pack/plugins/search_inference_endpoints/README.md b/x-pack/plugins/search_inference_endpoints/README.md new file mode 100644 index 0000000000000..3c28f965e7bf5 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/README.md @@ -0,0 +1,3 @@ +# Inference Endpoints + +The Inference Endpoints is a tool used to manage inference endpoints diff --git a/x-pack/plugins/search_inference_endpoints/common/constants.ts b/x-pack/plugins/search_inference_endpoints/common/constants.ts new file mode 100644 index 0000000000000..cdba227626a55 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/common/constants.ts @@ -0,0 +1,11 @@ +/* + * 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 const PLUGIN_ID = 'searchInferenceEndpoints'; +export const PLUGIN_NAME = 'InferenceEndpoints'; + +export const INFERENCE_ENDPOINTS_QUERY_KEY = 'inferenceEndpointsQueryKey'; diff --git a/x-pack/plugins/search_inference_endpoints/common/doc_links.ts b/x-pack/plugins/search_inference_endpoints/common/doc_links.ts new file mode 100644 index 0000000000000..c3d8d31dffa6d --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/common/doc_links.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 { DocLinks } from '@kbn/doc-links'; + +class InferenceEndpointsDocLinks { + public nlpImportModel: string = ''; + public supportedNlpModels: string = ''; + + constructor() {} + + setDocLinks(newDocLinks: DocLinks) { + this.nlpImportModel = newDocLinks.ml.nlpImportModel; + this.supportedNlpModels = newDocLinks.ml.supportedNlpModels; + } +} + +export const docLinks = new InferenceEndpointsDocLinks(); diff --git a/x-pack/plugins/search_inference_endpoints/common/translations.ts b/x-pack/plugins/search_inference_endpoints/common/translations.ts new file mode 100644 index 0000000000000..e58829812829b --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/common/translations.ts @@ -0,0 +1,96 @@ +/* + * 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 INFERENCE_ENDPOINT_LABEL = i18n.translate( + 'xpack.searchInferenceEndpoints.inferenceEndpointsLabel', + { + defaultMessage: 'Inference Endpoints', + } +); + +export const MANAGE_INFERENCE_ENDPOINTS_LABEL = i18n.translate( + 'xpack.searchInferenceEndpoints.allInferenceEndpoints.description', + { + defaultMessage: 'Manage your inference endpoints.', + } +); + +export const ADD_ENDPOINT_LABEL = i18n.translate( + 'xpack.searchInferenceEndpoints.newInferenceEndpointButtonLabel', + { + defaultMessage: 'Add endpoint', + } +); + +export const CREATE_FIRST_INFERENCE_ENDPOINT_DESCRIPTION = i18n.translate( + 'xpack.searchInferenceEndpoints.addEmptyPrompt.createFirstInferenceEndpointDescription', + { + defaultMessage: + 'Connect to your third-party model provider to create an inference endpoint for semantic search.', + } +); + +export const START_WITH_PREPARED_ENDPOINTS_LABEL = i18n.translate( + 'xpack.searchInferenceEndpoints.addEmptyPrompt.startWithPreparedEndpointsLabel', + { + defaultMessage: 'Get started quickly with our prepared endpoints:', + } +); + +export const ELSER_TITLE = i18n.translate( + 'xpack.searchInferenceEndpoints.addEmptyPrompt.elserTitle', + { + defaultMessage: 'ELSER', + } +); + +export const ELSER_DESCRIPTION = i18n.translate( + 'xpack.searchInferenceEndpoints.addEmptyPrompt.elserDescription', + { + defaultMessage: + 'ELSER is a sparse vector NLP model trained by Elastic for semantic search. Recommended for English language.', + } +); + +export const E5_TITLE = i18n.translate('xpack.searchInferenceEndpoints.addEmptyPrompt.e5Title', { + defaultMessage: 'Multilingual E5', +}); + +export const E5_DESCRIPTION = i18n.translate( + 'xpack.searchInferenceEndpoints.addEmptyPrompt.e5Description', + { + defaultMessage: + 'E5 is a dense vector NLP model that enables you to perform multi-lingual semantic search.', + } +); + +export const ERROR_TITLE = i18n.translate('xpack.searchInferenceEndpoints.inferenceId.errorTitle', { + defaultMessage: 'Error adding inference endpoint', +}); + +export const UNABLE_TO_CREATE_INFERENCE_ENDPOINT = i18n.translate( + 'xpack.searchInferenceEndpoints.inferenceFlyoutWrapperComponent.unableTocreateInferenceEndpointError', + { + defaultMessage: 'Unable to create an inference endpoint.', + } +); + +export const INFERENCE_ENDPOINT_ALREADY_EXISTS = i18n.translate( + 'xpack.searchInferenceEndpoints.inferenceFlyoutWrapperComponent.inferenceEndpointAlreadyExistsError', + { + defaultMessage: 'Inference Endpoint id already exists', + } +); + +export const FORBIDDEN_TO_ACCESS_TRAINED_MODELS = i18n.translate( + 'xpack.searchInferenceEndpoints.inferenceFlyoutWrapperComponent.forbiddenToAccessTrainedModelsError', + { + defaultMessage: 'Forbidden to access trained models', + } +); diff --git a/x-pack/plugins/search_inference_endpoints/common/types.ts b/x-pack/plugins/search_inference_endpoints/common/types.ts new file mode 100644 index 0000000000000..ef29d987309f8 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/common/types.ts @@ -0,0 +1,16 @@ +/* + * 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 enum APIRoutes { + GET_INFERENCE_ENDPOINTS = '/internal/inference_endpoints/endpoints', +} + +export interface SearchInferenceEndpointsConfigType { + ui: { + enabled: boolean; + }; +} diff --git a/x-pack/plugins/search_inference_endpoints/jest.config.js b/x-pack/plugins/search_inference_endpoints/jest.config.js new file mode 100644 index 0000000000000..8c19afbe9fb98 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/jest.config.js @@ -0,0 +1,18 @@ +/* + * 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. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/search_inference_endpoints'], + coverageDirectory: + '/target/kibana-coverage/jest/x-pack/plugins/search_inference_endpoints', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/x-pack/plugins/search_inference_endpoints/{public,server}/**/*.{ts,tsx}', + ], +}; diff --git a/x-pack/plugins/search_inference_endpoints/kibana.jsonc b/x-pack/plugins/search_inference_endpoints/kibana.jsonc new file mode 100644 index 0000000000000..6cdca96205588 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/kibana.jsonc @@ -0,0 +1,26 @@ +{ + "type": "plugin", + "id": "@kbn/search-inference-endpoints", + "owner": "@elastic/search-kibana", + "plugin": { + "id": "searchInferenceEndpoints", + "server": true, + "browser": true, + "configPath": [ + "xpack", + "searchInferenceEndpoints" + ], + "requiredPlugins": [ + "actions", + "share", + ], + "optionalPlugins": [ + "cloud", + "console", + "ml" + ], + "requiredBundles": [ + "kibanaReact" + ] + } +} diff --git a/x-pack/plugins/search_inference_endpoints/public/application.tsx b/x-pack/plugins/search_inference_endpoints/public/application.tsx new file mode 100644 index 0000000000000..c14f24a678281 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/application.tsx @@ -0,0 +1,38 @@ +/* + * 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 ReactDOM from 'react-dom'; +import { CoreStart } from '@kbn/core/public'; +import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { I18nProvider } from '@kbn/i18n-react'; +import { Router } from '@kbn/shared-ux-router'; +import { AppPluginStartDependencies } from './types'; + +export const renderApp = async ( + core: CoreStart, + services: AppPluginStartDependencies, + element: HTMLElement +) => { + const { InferenceEndpointsRouter } = await import('./inference_endpoints_router'); + + ReactDOM.render( + + + + + + + + + , + element + ); + + return () => ReactDOM.unmountComponentAtNode(element); +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/assets/images/inference_endpoint.svg b/x-pack/plugins/search_inference_endpoints/public/assets/images/inference_endpoint.svg new file mode 100644 index 0000000000000..927eccf9106a6 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/assets/images/inference_endpoint.svg @@ -0,0 +1,4338 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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 new file mode 100644 index 0000000000000..1b7e72149fd43 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/constants.ts @@ -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 { + SortFieldInferenceEndpoint, + QueryParams, + AlInferenceEndpointsTableState, + SortOrder, +} from './types'; + +export const DEFAULT_TABLE_ACTIVE_PAGE = 1; +export const DEFAULT_TABLE_LIMIT = 10; + +export const DEFAULT_QUERY_PARAMS: QueryParams = { + page: DEFAULT_TABLE_ACTIVE_PAGE, + perPage: DEFAULT_TABLE_LIMIT, + sortField: SortFieldInferenceEndpoint.endpoint, + sortOrder: SortOrder.asc, +}; + +export const DEFAULT_INFERENCE_ENDPOINTS_TABLE_STATE: AlInferenceEndpointsTableState = { + queryParams: DEFAULT_QUERY_PARAMS, +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/endpoints_table.tsx b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/endpoints_table.tsx new file mode 100644 index 0000000000000..af4755c72fc11 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/endpoints_table.tsx @@ -0,0 +1,40 @@ +/* + * 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 type { EuiBasicTableProps, Pagination } from '@elastic/eui'; +import { EuiBasicTable } from '@elastic/eui'; + +import type { InferenceEndpointUI } from './types'; + +interface EndpointsTableProps { + columns: EuiBasicTableProps['columns']; + data: InferenceEndpointUI[]; + onChange: EuiBasicTableProps['onChange']; + pagination: Pagination; + sorting: EuiBasicTableProps['sorting']; +} + +export const EndpointsTable: React.FC = ({ + columns, + data, + onChange, + pagination, + sorting, +}) => { + return ( + + ); +}; 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 new file mode 100644 index 0000000000000..39e957f908684 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/table_columns.ts @@ -0,0 +1,35 @@ +/* + * 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 new file mode 100644 index 0000000000000..091ff8270dd63 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.test.tsx @@ -0,0 +1,49 @@ +/* + * 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 { screen } from '@testing-library/react'; +import { render } from '@testing-library/react'; +import { TabularPage } from './tabular_page'; +import { InferenceAPIConfigResponse } from '@kbn/ml-trained-models-utils'; + +const inferenceEndpoints = [ + { + model_id: 'my-elser-model-05', + task_type: 'sparse_embedding', + service: 'elser', + service_settings: { + num_allocations: 1, + num_threads: 1, + model_id: '.elser_model_2', + }, + task_settings: {}, + }, + { + model_id: 'my-elser-model-04', + task_type: 'sparse_embedding', + service: 'elser', + service_settings: { + num_allocations: 1, + num_threads: 1, + model_id: '.elser_model_2', + }, + task_settings: {}, + }, +] as InferenceAPIConfigResponse[]; + +describe('When the tabular page is loaded', () => { + beforeEach(() => { + render(); + }); + + it('should display all model_ids in the table', () => { + const rows = screen.getAllByRole('row'); + expect(rows[1]).toHaveTextContent('my-elser-model-04'); + expect(rows[2]).toHaveTextContent('my-elser-model-05'); + }); +}); 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 new file mode 100644 index 0000000000000..2fb84dc4b99de --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/tabular_page.tsx @@ -0,0 +1,57 @@ +/* + * 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, { useCallback } from 'react'; + +import { InferenceAPIConfigResponse } from '@kbn/ml-trained-models-utils'; + +import { useTableData } from '../../hooks/use_table_data'; + +import { useAllInferenceEndpointsState } from '../../hooks/use_all_inference_endpoints_state'; +import { EndpointsTable } from './endpoints_table'; +import { TABLE_COLUMNS } from './table_columns'; + +interface TabularPageProps { + inferenceEndpoints: InferenceAPIConfigResponse[]; +} + +export const TabularPage: React.FC = ({ inferenceEndpoints }) => { + const { queryParams, setQueryParams } = useAllInferenceEndpointsState(); + + const { paginatedSortedTableData, pagination, sorting } = useTableData( + inferenceEndpoints, + queryParams + ); + + const handleTableChange = useCallback( + ({ page, sort }) => { + const newQueryParams = { + ...queryParams, + ...(sort && { + sortField: sort.field, + sortOrder: sort.direction, + }), + ...(page && { + page: page.index + 1, + perPage: page.size, + }), + }; + setQueryParams(newQueryParams); + }, + [queryParams, setQueryParams] + ); + + 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 new file mode 100644 index 0000000000000..4afba1dda110a --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/all_inference_endpoints/types.ts @@ -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. + */ + +export const INFERENCE_ENDPOINTS_TABLE_PER_PAGE_VALUES = [10, 25, 50, 100]; + +export enum SortFieldInferenceEndpoint { + endpoint = 'endpoint', +} +export enum SortOrder { + asc = 'asc', + desc = 'desc', +} + +export interface SortingParams { + sortField: SortFieldInferenceEndpoint; + sortOrder: SortOrder; +} + +export interface QueryParams extends SortingParams { + page: number; + perPage: number; +} + +export interface AlInferenceEndpointsTableState { + queryParams: QueryParams; +} + +export interface EuiBasicTableSortTypes { + direction: SortOrder; + field: string; +} + +export interface InferenceEndpointUI { + endpoint: string; + provider: string; + type: string; +} diff --git a/x-pack/plugins/search_inference_endpoints/public/components/app.tsx b/x-pack/plugins/search_inference_endpoints/public/components/app.tsx new file mode 100644 index 0000000000000..30c74898680a0 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/app.tsx @@ -0,0 +1,13 @@ +/* + * 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 { InferenceEndpoints } from './inference_endpoints'; + +export const App: React.FC = () => { + return ; +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/add_empty_prompt.scss b/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/add_empty_prompt.scss new file mode 100644 index 0000000000000..b85859948b0be --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/add_empty_prompt.scss @@ -0,0 +1,3 @@ +.addEmptyPrompt { + max-width: 860px; +} \ No newline at end of file diff --git a/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/add_empty_prompt.test.tsx b/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/add_empty_prompt.test.tsx new file mode 100644 index 0000000000000..f9b7e11e48d94 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/add_empty_prompt.test.tsx @@ -0,0 +1,33 @@ +/* + * 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 { fireEvent, screen } from '@testing-library/react'; +import { AddEmptyPrompt } from './add_empty_prompt'; + +import { renderReactTestingLibraryWithI18n as render } from '@kbn/test-jest-helpers'; +import '@testing-library/jest-dom'; +const setIsInferenceFlyoutVisibleMock = jest.fn(); + +describe('When empty prompt is loaded', () => { + beforeEach(() => { + render(); + }); + + it('should display the description for creation of the first inference endpoint', () => { + expect( + screen.getByText( + /Connect to your third-party model provider to create an inference endpoint for semantic search./ + ) + ).toBeInTheDocument(); + }); + + it('calls setIsInferenceFlyoutVisible when the addInferenceEndpoint button is clicked', async () => { + fireEvent.click(screen.getByTestId('addEndpointButtonForEmptyPrompt')); + expect(setIsInferenceFlyoutVisibleMock).toHaveBeenCalled(); + }); +}); 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 new file mode 100644 index 0000000000000..69d74724016d6 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/add_empty_prompt.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 React from 'react'; + +import { + EuiButton, + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiImage, + EuiSpacer, +} from '@elastic/eui'; + +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 './add_empty_prompt.scss'; + +interface AddEmptyPromptProps { + setIsInferenceFlyoutVisible: (value: boolean) => void; +} + +export const AddEmptyPrompt: React.FC = ({ setIsInferenceFlyoutVisible }) => { + return ( + {i18n.INFERENCE_ENDPOINT_LABEL}} + body={ + + + {i18n.CREATE_FIRST_INFERENCE_ENDPOINT_DESCRIPTION} + + +
+ setIsInferenceFlyoutVisible(true)} + > + {i18n.ADD_ENDPOINT_LABEL} + +
+
+
+ } + footer={ + + + {i18n.START_WITH_PREPARED_ENDPOINTS_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 new file mode 100644 index 0000000000000..db38b649fd66e --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/elser_prompt.tsx @@ -0,0 +1,31 @@ +/* + * 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/index.ts b/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/index.ts new file mode 100644 index 0000000000000..dcb2d12f6c986 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { AddEmptyPrompt } from './add_empty_prompt'; 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/multilingual_e5_prompt.tsx new file mode 100644 index 0000000000000..69133909efcb2 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt/multilingual_e5_prompt.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 { EuiButton, EuiCard } from '@elastic/eui'; + +import * as i18n from '../../../common/translations'; + +interface MultilingualE5PromptProps { + setIsInferenceFlyoutVisible: (value: boolean) => void; +} + +export const MultilingualE5Prompt: React.FC = ({ + setIsInferenceFlyoutVisible, +}) => ( + setIsInferenceFlyoutVisible(true)}> + {i18n.ADD_ENDPOINT_LABEL} + + } + /> +); diff --git a/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt_page.tsx b/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt_page.tsx new file mode 100644 index 0000000000000..ec958266e4e1f --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/empty_prompt_page.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { AddEmptyPrompt } from './empty_prompt/add_empty_prompt'; + +interface EmptyPromptPageProps { + setIsInferenceFlyoutVisible: (value: boolean) => void; +} + +export const EmptyPromptPage: React.FC = ({ + setIsInferenceFlyoutVisible, +}) => ; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/endpoints_router.tsx b/x-pack/plugins/search_inference_endpoints/public/components/endpoints_router.tsx new file mode 100644 index 0000000000000..775a4efdbc15c --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/endpoints_router.tsx @@ -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 React from 'react'; + +import { Route, Routes } from '@kbn/shared-ux-router'; + +import { INFERENCE_ENDPOINTS_PATH } from './routes'; + +import { InferenceEndpoints } from './inference_endpoints'; + +export const EndpointsRouter: React.FC = () => { + return ( + + + + + + ); +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/inference_endpoints.tsx b/x-pack/plugins/search_inference_endpoints/public/components/inference_endpoints.tsx new file mode 100644 index 0000000000000..0fa200991b8d1 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/inference_endpoints.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, { useState } from 'react'; + +import { EuiPageTemplate } from '@elastic/eui'; + +import { useQueryInferenceEndpoints } from '../hooks/use_inference_endpoints'; +import { TabularPage } from './all_inference_endpoints/tabular_page'; +import { EmptyPromptPage } from './empty_prompt_page'; +import { InferenceEndpointsHeader } from './inference_endpoints_header'; +import { InferenceFlyoutWrapperComponent } from './inference_flyout_wrapper_component'; + +export const InferenceEndpoints: React.FC = () => { + const { inferenceEndpoints } = useQueryInferenceEndpoints(); + const [isInferenceFlyoutVisible, setIsInferenceFlyoutVisible] = useState(false); + + return ( + <> + {inferenceEndpoints.length > 0 && ( + + )} + + {inferenceEndpoints.length === 0 ? ( + + ) : ( + + )} + + {isInferenceFlyoutVisible && ( + + )} + + ); +}; 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 new file mode 100644 index 0000000000000..8b551af28bd4c --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/inference_endpoints_header.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 React from 'react'; +import { EuiPageTemplate, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; +import * as i18n from '../../common/translations'; + +interface InferenceEndpointsHeaderProps { + setIsInferenceFlyoutVisible: (isVisible: boolean) => void; +} + +export const InferenceEndpointsHeader: React.FC = ({ + setIsInferenceFlyoutVisible, +}) => ( + .euiFlexGroup': { flexWrap: 'wrap' } }} + data-test-subj="allInferenceEndpointsPage" + pageTitle={i18n.INFERENCE_ENDPOINT_LABEL} + description={i18n.MANAGE_INFERENCE_ENDPOINTS_LABEL} + rightSideItems={[ + + + 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 new file mode 100644 index 0000000000000..bf1343fe3db8b --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/inference_flyout_wrapper_component.tsx @@ -0,0 +1,172 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexItem, EuiCallOut, EuiText, EuiSpacer } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { InferenceFlyoutWrapper } from '@kbn/inference_integration_flyout/components/inference_flyout_wrapper'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; + +import { InferenceTaskType } from '@elastic/elasticsearch/lib/api/types'; + +import { ModelConfig } from '@kbn/inference_integration_flyout/types'; +import { extractErrorProperties } from '@kbn/ml-error-utils'; +import { TrainedModelConfigResponse } from '@kbn/ml-plugin/common/types/trained_models'; +import { SUPPORTED_PYTORCH_TASKS, TRAINED_MODEL_TYPE } from '@kbn/ml-trained-models-utils'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { InferenceAPIConfigResponse } from '@kbn/ml-trained-models-utils'; +import * as i18n from '../../common/translations'; +import { INFERENCE_ENDPOINTS_QUERY_KEY } from '../../common/constants'; +import { useKibana } from '../hooks/use_kibana'; +import { docLinks } from '../../common/doc_links'; + +interface InferenceFlyoutWrapperComponentProps { + inferenceEndpoints: InferenceAPIConfigResponse[]; + isInferenceFlyoutVisible: boolean; + setIsInferenceFlyoutVisible: (isVisible: boolean) => void; +} + +export const InferenceFlyoutWrapperComponent: React.FC = ({ + inferenceEndpoints, + isInferenceFlyoutVisible, + setIsInferenceFlyoutVisible, +}) => { + const [inferenceAddError, setInferenceAddError] = useState(undefined); + const [isCreateInferenceApiLoading, setIsCreateInferenceApiLoading] = useState(false); + const [availableTrainedModels, setAvailableTrainedModels] = useState< + TrainedModelConfigResponse[] + >([]); + + const [inferenceEndpointError, setInferenceEndpointError] = useState( + undefined + ); + + const queryClient = useQueryClient(); + + const { + services: { ml }, + } = useKibana(); + + const createInferenceEndpointMutation = useMutation( + async ({ + inferenceId, + taskType, + modelConfig, + }: { + inferenceId: string; + taskType: InferenceTaskType; + modelConfig: ModelConfig; + }) => { + if (!ml) { + throw new Error(i18n.UNABLE_TO_CREATE_INFERENCE_ENDPOINT); + } + await ml?.mlApi?.inferenceModels?.createInferenceEndpoint(inferenceId, taskType, modelConfig); + }, + { + onSuccess: () => { + queryClient.invalidateQueries([INFERENCE_ENDPOINTS_QUERY_KEY]); + }, + } + ); + + const onInferenceEndpointChange = useCallback( + async (inferenceId: string) => { + const modelsExist = inferenceEndpoints.some((i) => i.model_id === inferenceId); + if (modelsExist) { + setInferenceEndpointError(i18n.INFERENCE_ENDPOINT_ALREADY_EXISTS); + } else { + setInferenceEndpointError(undefined); + } + }, + [inferenceEndpoints] + ); + + const onSaveInferenceCallback = useCallback( + async (inferenceId: string, taskType: InferenceTaskType, modelConfig: ModelConfig) => { + setIsCreateInferenceApiLoading(true); + try { + await createInferenceEndpointMutation.mutateAsync({ inferenceId, taskType, modelConfig }); + setIsInferenceFlyoutVisible(!isInferenceFlyoutVisible); + } catch (error) { + const errorObj = extractErrorProperties(error); + setInferenceAddError(errorObj.message); + } finally { + setIsCreateInferenceApiLoading(false); + } + }, + [createInferenceEndpointMutation, isInferenceFlyoutVisible, setIsInferenceFlyoutVisible] + ); + + const onFlyoutClose = useCallback(() => { + setInferenceAddError(undefined); + setIsInferenceFlyoutVisible(!isInferenceFlyoutVisible); + }, [isInferenceFlyoutVisible, setIsInferenceFlyoutVisible]); + + useEffect(() => { + const fetchAvailableTrainedModels = async () => { + let models; + try { + models = await ml?.mlApi?.trainedModels?.getTrainedModels(); + } catch (error) { + const errorObj = extractErrorProperties(error); + if (errorObj.statusCode === 403) { + setInferenceAddError(i18n.FORBIDDEN_TO_ACCESS_TRAINED_MODELS); + } else { + setInferenceAddError(errorObj.message); + } + } finally { + setAvailableTrainedModels(models || []); + } + }; + + fetchAvailableTrainedModels(); + }, [ml]); + + const trainedModels = useMemo(() => { + const availableTrainedModelsList = availableTrainedModels + .filter( + (model: TrainedModelConfigResponse) => + model.model_type === TRAINED_MODEL_TYPE.PYTORCH && + (model?.inference_config + ? Object.keys(model.inference_config).includes(SUPPORTED_PYTORCH_TASKS.TEXT_EMBEDDING) + : {}) + ) + .map((model: TrainedModelConfigResponse) => model.model_id); + + return availableTrainedModelsList; + }, [availableTrainedModels]); + + return ( + + + + + + + + + ) + } + onInferenceEndpointChange={onInferenceEndpointChange} + inferenceEndpointError={inferenceEndpointError} + trainedModels={trainedModels} + onSaveInferenceEndpoint={onSaveInferenceCallback} + onFlyoutClose={onFlyoutClose} + isInferenceFlyoutVisible={isInferenceFlyoutVisible} + supportedNlpModels={docLinks.supportedNlpModels} + nlpImportModel={docLinks.nlpImportModel} + isCreateInferenceApiLoading={isCreateInferenceApiLoading} + setInferenceEndpointError={setInferenceEndpointError} + /> + ); +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/components/routes.ts b/x-pack/plugins/search_inference_endpoints/public/components/routes.ts new file mode 100644 index 0000000000000..032b8c96e43d0 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/components/routes.ts @@ -0,0 +1,11 @@ +/* + * 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 const ROOT_PATH = '/'; +export const SETUP_GUIDE_PATH = '/setup_guide'; +export const ERROR_STATE_PATH = '/error_state'; +export const INFERENCE_ENDPOINTS_PATH = `${ROOT_PATH}inference_endpoints`; diff --git a/x-pack/plugins/search_inference_endpoints/public/embeddable.tsx b/x-pack/plugins/search_inference_endpoints/public/embeddable.tsx new file mode 100644 index 0000000000000..fb733ec7ff5e6 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/embeddable.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 React from 'react'; +import { dynamic } from '@kbn/shared-ux-utility'; +import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; +import { CoreStart } from '@kbn/core-lifecycle-browser'; +import { AppPluginStartDependencies } from './types'; + +export const InferenceEndpoints = dynamic(async () => ({ + default: (await import('./components/app')).App, +})); + +export const InferenceEndpointsProvider = dynamic(async () => ({ + default: (await import('./providers/inference_endpoints_provider')).InferenceEndpointsProvider, +})); + +export const getInferenceEndpointsProvider = + (core: CoreStart, services: AppPluginStartDependencies) => + (props: React.ComponentProps) => + ( + + + + ); 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 new file mode 100644 index 0000000000000..4979cdf7994fc --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/hooks/use_all_inference_endpoints_state.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 { useCallback, useState } from 'react'; + +import type { + QueryParams, + AlInferenceEndpointsTableState, +} from '../components/all_inference_endpoints/types'; + +import { DEFAULT_INFERENCE_ENDPOINTS_TABLE_STATE } from '../components/all_inference_endpoints/constants'; + +interface UseAllInferenceEndpointsStateReturn { + queryParams: QueryParams; + setQueryParams: (queryParam: Partial) => void; +} + +export function useAllInferenceEndpointsState(): UseAllInferenceEndpointsStateReturn { + const [tableState, setTableState] = useState( + DEFAULT_INFERENCE_ENDPOINTS_TABLE_STATE + ); + const setState = useCallback((state: AlInferenceEndpointsTableState) => { + setTableState(state); + }, []); + + return { + queryParams: { + ...DEFAULT_INFERENCE_ENDPOINTS_TABLE_STATE.queryParams, + ...tableState.queryParams, + }, + setQueryParams: (newQueryParams: Partial) => { + setState({ + queryParams: { ...tableState.queryParams, ...newQueryParams }, + }); + }, + }; +} diff --git a/x-pack/plugins/search_inference_endpoints/public/hooks/use_inference_endpoints.ts b/x-pack/plugins/search_inference_endpoints/public/hooks/use_inference_endpoints.ts new file mode 100644 index 0000000000000..f400429bec250 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/hooks/use_inference_endpoints.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 { useQuery } from '@tanstack/react-query'; +import { InferenceAPIConfigResponse } from '@kbn/ml-trained-models-utils'; +import { APIRoutes } from '../types'; +import { useKibana } from './use_kibana'; +import { INFERENCE_ENDPOINTS_QUERY_KEY } from '../../common/constants'; + +export const useQueryInferenceEndpoints = (): { + inferenceEndpoints: InferenceAPIConfigResponse[]; + isLoading: boolean; +} => { + const { services } = useKibana(); + + const { data, isLoading } = useQuery({ + queryKey: [INFERENCE_ENDPOINTS_QUERY_KEY], + queryFn: async () => { + const response = await services.http.get<{ + inference_endpoints: InferenceAPIConfigResponse[]; + }>(APIRoutes.GET_INFERENCE_ENDPOINTS, {}); + + return response.inference_endpoints; + }, + }); + + return { inferenceEndpoints: data || [], isLoading }; +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/hooks/use_kibana.ts b/x-pack/plugins/search_inference_endpoints/public/hooks/use_kibana.ts new file mode 100644 index 0000000000000..90fa7597e17dc --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/hooks/use_kibana.ts @@ -0,0 +1,11 @@ +/* + * 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 { useKibana as _useKibana } from '@kbn/kibana-react-plugin/public'; +import { AppServicesContext } from '../types'; + +export const useKibana = () => _useKibana(); 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 new file mode 100644 index 0000000000000..e0026cbffff98 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/hooks/use_table_data.test.tsx @@ -0,0 +1,92 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { QueryParams } from '../components/all_inference_endpoints/types'; +import { useTableData } from './use_table_data'; +import { INFERENCE_ENDPOINTS_TABLE_PER_PAGE_VALUES } from '../components/all_inference_endpoints/types'; + +const inferenceEndpoints = [ + { + model_id: 'my-elser-model-04', + task_type: 'sparse_embedding', + service: 'elser', + service_settings: { + num_allocations: 1, + num_threads: 1, + model_id: '.elser_model_2', + }, + task_settings: {}, + }, + { + model_id: 'my-elser-model-01', + task_type: 'sparse_embedding', + service: 'elser', + service_settings: { + num_allocations: 1, + num_threads: 1, + model_id: '.elser_model_2', + }, + task_settings: {}, + }, + { + model_id: 'my-elser-model-05', + task_type: 'sparse_embedding', + service: 'elser', + service_settings: { + num_allocations: 1, + num_threads: 1, + model_id: '.elser_model_2', + }, + task_settings: {}, + }, +] as InferenceAPIConfigResponse[]; + +const queryParams = { + page: 1, + perPage: 10, + sortField: 'endpoint', + sortOrder: 'desc', +} as QueryParams; + +describe('useTableData', () => { + it('should return correct pagination', () => { + const { result } = renderHook(() => useTableData(inferenceEndpoints, queryParams)); + + expect(result.current.pagination).toEqual({ + pageIndex: 0, + pageSize: 10, + pageSizeOptions: INFERENCE_ENDPOINTS_TABLE_PER_PAGE_VALUES, + totalItemCount: 3, + }); + }); + + it('should return correct sorting', () => { + const { result } = renderHook(() => useTableData(inferenceEndpoints, queryParams)); + + expect(result.current.sorting).toEqual({ + sort: { + direction: 'desc', + field: 'endpoint', + }, + }); + }); + + it('should return correctly sorted data', () => { + const { result } = renderHook(() => useTableData(inferenceEndpoints, queryParams)); + + const expectedSortedData = [...inferenceEndpoints].sort((a, b) => + b.model_id.localeCompare(a.model_id) + ); + + const sortedEndpoints = result.current.sortedTableData.map((item) => item.endpoint); + const expectedModelIds = expectedSortedData.map((item) => item.model_id); + + expect(sortedEndpoints).toEqual(expectedModelIds); + }); +}); 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 new file mode 100644 index 0000000000000..304c469e06093 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/hooks/use_table_data.tsx @@ -0,0 +1,81 @@ +/* + * 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 { EuiTableSortingType } from '@elastic/eui'; +import { Pagination } from '@elastic/eui'; +import { InferenceAPIConfigResponse } from '@kbn/ml-trained-models-utils'; +import { useMemo } from 'react'; +import { DEFAULT_TABLE_LIMIT } from '../components/all_inference_endpoints/constants'; +import { + InferenceEndpointUI, + INFERENCE_ENDPOINTS_TABLE_PER_PAGE_VALUES, + QueryParams, + SortOrder, +} from '../components/all_inference_endpoints/types'; + +interface UseTableDataReturn { + tableData: InferenceEndpointUI[]; + sortedTableData: InferenceEndpointUI[]; + paginatedSortedTableData: InferenceEndpointUI[]; + pagination: Pagination; + sorting: EuiTableSortingType; +} + +export const useTableData = ( + inferenceEndpoints: InferenceAPIConfigResponse[], + queryParams: QueryParams +): UseTableDataReturn => { + const tableData: InferenceEndpointUI[] = useMemo(() => { + return inferenceEndpoints.map((endpoint) => ({ + endpoint: endpoint.model_id, + provider: endpoint.service, + type: endpoint.task_type, + })); + }, [inferenceEndpoints]); + + const sortedTableData: InferenceEndpointUI[] = useMemo(() => { + return [...tableData].sort((a, b) => { + const aValue = a[queryParams.sortField]; + const bValue = b[queryParams.sortField]; + + if (queryParams.sortOrder === SortOrder.asc) { + return aValue.localeCompare(bValue); + } else { + return bValue.localeCompare(aValue); + } + }); + }, [tableData, queryParams]); + + const pagination: Pagination = useMemo( + () => ({ + pageIndex: queryParams.page - 1, + pageSize: queryParams.perPage, + pageSizeOptions: INFERENCE_ENDPOINTS_TABLE_PER_PAGE_VALUES, + totalItemCount: inferenceEndpoints.length ?? 0, + }), + [inferenceEndpoints, queryParams] + ); + + const paginatedSortedTableData: InferenceEndpointUI[] = useMemo(() => { + const pageSize = pagination.pageSize || DEFAULT_TABLE_LIMIT; + const startIndex = pagination.pageIndex * pageSize; + const endIndex = startIndex + pageSize; + return sortedTableData.slice(startIndex, endIndex); + }, [sortedTableData, pagination]); + + const sorting = useMemo( + () => ({ + sort: { + direction: queryParams.sortOrder, + field: queryParams.sortField, + }, + }), + [queryParams.sortField, queryParams.sortOrder] + ); + + return { tableData, sortedTableData, paginatedSortedTableData, pagination, sorting }; +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/index.ts b/x-pack/plugins/search_inference_endpoints/public/index.ts new file mode 100644 index 0000000000000..b06f1f64b909d --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/index.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 { PluginInitializerContext } from '@kbn/core/public'; + +import { SearchInferenceEndpointsPlugin } from './plugin'; + +export function plugin(initializerContext: PluginInitializerContext) { + return new SearchInferenceEndpointsPlugin(initializerContext); +} + +export type { + SearchInferenceEndpointsPluginSetup, + SearchInferenceEndpointsPluginStart, +} from './types'; diff --git a/x-pack/plugins/search_inference_endpoints/public/inference_endpoints_overview.tsx b/x-pack/plugins/search_inference_endpoints/public/inference_endpoints_overview.tsx new file mode 100644 index 0000000000000..5dd017f263caa --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/inference_endpoints_overview.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, { useMemo } from 'react'; + +import { EuiPageTemplate } from '@elastic/eui'; + +import { App } from './components/app'; +import { useKibana } from './hooks/use_kibana'; +import { InferenceEndpointsProvider } from './providers/inference_endpoints_provider'; + +export const InferenceEndpointsOverview: React.FC = () => { + const { + services: { console: consolePlugin }, + } = useKibana(); + + const embeddableConsole = useMemo( + () => (consolePlugin?.EmbeddableConsole ? : null), + [consolePlugin] + ); + + return ( + + + + {embeddableConsole} + + + ); +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/inference_endpoints_router.tsx b/x-pack/plugins/search_inference_endpoints/public/inference_endpoints_router.tsx new file mode 100644 index 0000000000000..7165b01993232 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/inference_endpoints_router.tsx @@ -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 { Route, Routes } from '@kbn/shared-ux-router'; +import React from 'react'; +import { Redirect } from 'react-router-dom'; +import { InferenceEndpointsOverview } from './inference_endpoints_overview'; + +import { ROOT_PATH, SEARCH_INFERENCE_ENDPOINTS_PATH } from './routes'; + +export const InferenceEndpointsRouter: React.FC = () => { + return ( + + + + + + + ); +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/plugin.ts b/x-pack/plugins/search_inference_endpoints/public/plugin.ts new file mode 100644 index 0000000000000..ab49281f82e4c --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/plugin.ts @@ -0,0 +1,71 @@ +/* + * 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 { + AppMountParameters, + CoreSetup, + CoreStart, + Plugin, + PluginInitializerContext, +} from '@kbn/core/public'; +import { PLUGIN_ID, PLUGIN_NAME } from '../common/constants'; +import { docLinks } from '../common/doc_links'; +import { InferenceEndpoints, getInferenceEndpointsProvider } from './embeddable'; +import { + AppPluginStartDependencies, + SearchInferenceEndpointsConfigType, + SearchInferenceEndpointsPluginSetup, + SearchInferenceEndpointsPluginStart, +} from './types'; + +export class SearchInferenceEndpointsPlugin + implements Plugin +{ + private config: SearchInferenceEndpointsConfigType; + + constructor(initializerContext: PluginInitializerContext) { + this.config = initializerContext.config.get(); + } + + public setup( + core: CoreSetup + ): SearchInferenceEndpointsPluginSetup { + if (!this.config.ui?.enabled) return {}; + + core.application.register({ + id: PLUGIN_ID, + appRoute: '/app/search_inference_endpoints', + title: PLUGIN_NAME, + async mount({ element, history }: AppMountParameters) { + const { renderApp } = await import('./application'); + const [coreStart, depsStart] = await core.getStartServices(); + const startDeps: AppPluginStartDependencies = { + ...depsStart, + history, + }; + + return renderApp(coreStart, startDeps, element); + }, + }); + + return {}; + } + + public start( + core: CoreStart, + deps: AppPluginStartDependencies + ): SearchInferenceEndpointsPluginStart { + docLinks.setDocLinks(core.docLinks.links); + + return { + InferenceEdnpointsProvider: getInferenceEndpointsProvider(core, deps), + InferenceEndpoints, + }; + } + + public stop() {} +} diff --git a/x-pack/plugins/search_inference_endpoints/public/providers/inference_endpoints_provider.tsx b/x-pack/plugins/search_inference_endpoints/public/providers/inference_endpoints_provider.tsx new file mode 100644 index 0000000000000..1813fbae2901e --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/providers/inference_endpoints_provider.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 { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import React, { FC, PropsWithChildren } from 'react'; + +const queryClient = new QueryClient({}); + +export interface InferenceEndpointsProviderProps { + children: React.ReactNode; +} + +export const InferenceEndpointsProvider: FC> = ({ + children, +}) => { + return {children}; +}; diff --git a/x-pack/plugins/search_inference_endpoints/public/routes.ts b/x-pack/plugins/search_inference_endpoints/public/routes.ts new file mode 100644 index 0000000000000..d2904473400ee --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/routes.ts @@ -0,0 +1,9 @@ +/* + * 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 const ROOT_PATH = '/'; +export const SEARCH_INFERENCE_ENDPOINTS_PATH = `${ROOT_PATH}inference_endpoints`; diff --git a/x-pack/plugins/search_inference_endpoints/public/types.ts b/x-pack/plugins/search_inference_endpoints/public/types.ts new file mode 100644 index 0000000000000..6ac241fbe87b0 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/public/types.ts @@ -0,0 +1,36 @@ +/* + * 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 { ConsolePluginStart } from '@kbn/console-plugin/public'; +import { HttpStart } from '@kbn/core-http-browser'; +import { AppMountParameters } from '@kbn/core/public'; +import { MlPluginStart } from '@kbn/ml-plugin/public'; +import { SharePluginStart } from '@kbn/share-plugin/public'; +import React from 'react'; +import type { App } from './components/app'; +import type { InferenceEndpointsProvider } from './providers/inference_endpoints_provider'; + +export * from '../common/types'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SearchInferenceEndpointsPluginSetup {} +export interface SearchInferenceEndpointsPluginStart { + InferenceEdnpointsProvider: React.FC>; + InferenceEndpoints: React.FC>; +} + +export interface AppPluginStartDependencies { + history: AppMountParameters['history']; + share: SharePluginStart; + console?: ConsolePluginStart; +} + +export interface AppServicesContext { + http: HttpStart; + ml?: MlPluginStart; + console?: ConsolePluginStart; +} diff --git a/x-pack/plugins/search_inference_endpoints/server/config.ts b/x-pack/plugins/search_inference_endpoints/server/config.ts new file mode 100644 index 0000000000000..5593fb2251aba --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/server/config.ts @@ -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 { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from '@kbn/core/server'; + +export * from './types'; + +const configSchema = schema.object({ + enabled: schema.boolean({ defaultValue: true }), + ui: schema.object({ + enabled: schema.boolean({ defaultValue: false }), + }), +}); + +export type SearchInferenceEndpointsConfig = TypeOf; + +export const config: PluginConfigDescriptor = { + exposeToBrowser: { + ui: true, + }, + schema: configSchema, +}; diff --git a/x-pack/plugins/search_inference_endpoints/server/index.ts b/x-pack/plugins/search_inference_endpoints/server/index.ts new file mode 100644 index 0000000000000..10fa965c40457 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/server/index.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 { PluginInitializerContext } from '@kbn/core/server'; + +export { config } from './config'; + +export async function plugin(initializerContext: PluginInitializerContext) { + const { SearchInferenceEndpointsPlugin } = await import('./plugin'); + return new SearchInferenceEndpointsPlugin(initializerContext); +} + +export type { + SearchInferenceEndpointsPluginSetup, + SearchInferenceEndpointsPluginStart, +} from './types'; diff --git a/x-pack/plugins/search_inference_endpoints/server/lib/fetch_inference_endpoints.test.ts b/x-pack/plugins/search_inference_endpoints/server/lib/fetch_inference_endpoints.test.ts new file mode 100644 index 0000000000000..ee282f3056caa --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/server/lib/fetch_inference_endpoints.test.ts @@ -0,0 +1,68 @@ +/* + * 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'; + +import { fetchInferenceEndpoints } from './fetch_inference_endpoints'; + +describe('fetch indices', () => { + const mockInferenceEndpointsResponse = [ + { + model_id: 'my-elser-model-03', + task_type: 'sparse_embedding', + service: 'elser', + service_settings: { num_allocations: 1, num_threads: 1, model_id: '.elser_model_2' }, + task_settings: {}, + }, + { + model_id: 'my-elser-model-04', + task_type: 'sparse_embedding', + service: 'elser', + service_settings: { num_allocations: 1, num_threads: 1, model_id: '.elser_model_2' }, + task_settings: {}, + }, + { + model_id: 'my-elser-model-05', + task_type: 'sparse_embedding', + service: 'elser', + service_settings: { num_allocations: 1, num_threads: 1, model_id: '.elser_model_2' }, + task_settings: {}, + }, + { + model_id: 'my-elser-model-06', + task_type: 'sparse_embedding', + service: 'elser', + service_settings: { num_allocations: 1, num_threads: 1, model_id: '.elser_model_2' }, + task_settings: {}, + }, + ]; + beforeEach(() => { + jest.clearAllMocks(); + }); + + const mockClient = { + asCurrentUser: { + transport: { + request: jest.fn(), + }, + }, + }; + + it('returns all inference endpoints', async () => { + mockClient.asCurrentUser.transport.request.mockImplementationOnce(() => { + return Promise.resolve({ endpoints: mockInferenceEndpointsResponse }); + }); + + const indexData = await fetchInferenceEndpoints( + mockClient.asCurrentUser as unknown as ElasticsearchClient + ); + + expect(indexData).toEqual({ + inferenceEndpoints: mockInferenceEndpointsResponse, + }); + }); +}); diff --git a/x-pack/plugins/search_inference_endpoints/server/lib/fetch_inference_endpoints.ts b/x-pack/plugins/search_inference_endpoints/server/lib/fetch_inference_endpoints.ts new file mode 100644 index 0000000000000..69b38f0d3bdb3 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/server/lib/fetch_inference_endpoints.ts @@ -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 { ElasticsearchClient } from '@kbn/core/server'; +import { InferenceAPIConfigResponse } from '@kbn/ml-trained-models-utils'; + +export const fetchInferenceEndpoints = async ( + client: ElasticsearchClient +): Promise<{ + inferenceEndpoints: InferenceAPIConfigResponse[]; +}> => { + const { endpoints } = await client.transport.request<{ + endpoints: any; + }>({ + method: 'GET', + path: `/_inference/_all`, + }); + + return { + inferenceEndpoints: endpoints, + }; +}; diff --git a/x-pack/plugins/search_inference_endpoints/server/plugin.ts b/x-pack/plugins/search_inference_endpoints/server/plugin.ts new file mode 100644 index 0000000000000..74a877824956f --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/server/plugin.ts @@ -0,0 +1,50 @@ +/* + * 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 { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from '@kbn/core/server'; +import { defineRoutes } from './routes'; +import { + SearchInferenceEndpointsPluginSetup, + SearchInferenceEndpointsPluginStart, + SearchInferenceEndpointsPluginStartDependencies, +} from './types'; + +export class SearchInferenceEndpointsPlugin + implements + Plugin< + SearchInferenceEndpointsPluginSetup, + SearchInferenceEndpointsPluginStart, + {}, + SearchInferenceEndpointsPluginStartDependencies + > +{ + private readonly logger: Logger; + + constructor(initializerContext: PluginInitializerContext) { + this.logger = initializerContext.logger.get(); + } + + public setup( + core: CoreSetup< + SearchInferenceEndpointsPluginStartDependencies, + SearchInferenceEndpointsPluginStart + > + ) { + this.logger.debug('searchInferenceEndpoints: Setup'); + const router = core.http.createRouter(); + + defineRoutes({ logger: this.logger, router }); + + return {}; + } + + public start(core: CoreStart) { + return {}; + } + + public stop() {} +} diff --git a/x-pack/plugins/search_inference_endpoints/server/routes.ts b/x-pack/plugins/search_inference_endpoints/server/routes.ts new file mode 100644 index 0000000000000..d5f010b902c52 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/server/routes.ts @@ -0,0 +1,35 @@ +/* + * 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 { IRouter } from '@kbn/core/server'; +import type { Logger } from '@kbn/logging'; +import { fetchInferenceEndpoints } from './lib/fetch_inference_endpoints'; +import { APIRoutes } from './types'; +import { errorHandler } from './utils/error_handler'; + +export function defineRoutes({ logger, router }: { logger: Logger; router: IRouter }) { + router.get( + { + path: APIRoutes.GET_INFERENCE_ENDPOINTS, + validate: {}, + }, + errorHandler(logger)(async (context, request, response) => { + const { + client: { asCurrentUser }, + } = (await context.core).elasticsearch; + + const { inferenceEndpoints } = await fetchInferenceEndpoints(asCurrentUser); + + return response.ok({ + body: { + inference_endpoints: inferenceEndpoints, + }, + headers: { 'content-type': 'application/json' }, + }); + }) + ); +} diff --git a/x-pack/plugins/search_inference_endpoints/server/types.ts b/x-pack/plugins/search_inference_endpoints/server/types.ts new file mode 100644 index 0000000000000..fcc6ffa55ec0f --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/server/types.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 { PluginStartContract as ActionsPluginStartContract } from '@kbn/actions-plugin/server'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SearchInferenceEndpointsPluginSetup {} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SearchInferenceEndpointsPluginStart {} + +export interface SearchInferenceEndpointsPluginStartDependencies { + actions: ActionsPluginStartContract; +} + +export * from '../common/types'; diff --git a/x-pack/plugins/search_inference_endpoints/server/utils/error_handler.ts b/x-pack/plugins/search_inference_endpoints/server/utils/error_handler.ts new file mode 100644 index 0000000000000..3772786335fa8 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/server/utils/error_handler.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 { RequestHandlerWrapper } from '@kbn/core-http-server'; +import type { Logger } from '@kbn/logging'; + +export const errorHandler: (logger: Logger) => RequestHandlerWrapper = (logger) => (handler) => { + return async (context, request, response) => { + try { + return await handler(context, request, response); + } catch (e) { + logger.error(e); + throw e; + } + }; +}; diff --git a/x-pack/plugins/search_inference_endpoints/tsconfig.json b/x-pack/plugins/search_inference_endpoints/tsconfig.json new file mode 100644 index 0000000000000..a61af6c752250 --- /dev/null +++ b/x-pack/plugins/search_inference_endpoints/tsconfig.json @@ -0,0 +1,38 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + }, + "include": [ + "__mocks__/**/*", + "common/**/*", + "public/**/*", + "server/**/*" + ], + "kbn_references": [ + "@kbn/config-schema", + "@kbn/core", + "@kbn/core-http-browser", + "@kbn/i18n", + "@kbn/i18n-react", + "@kbn/kibana-react-plugin", + "@kbn/inference_integration_flyout", + "@kbn/ml-plugin", + "@kbn/ml-trained-models-utils", + "@kbn/shared-ux-router", + "@kbn/core-http-server", + "@kbn/share-plugin", + "@kbn/actions-plugin", + "@kbn/shared-ux-utility", + "@kbn/core-lifecycle-browser", + "@kbn/logging", + "@kbn/react-kibana-context-render", + "@kbn/doc-links", + "@kbn/console-plugin", + "@kbn/test-jest-helpers", + "@kbn/ml-error-utils" + ], + "exclude": [ + "target/**/*", + ] +} diff --git a/x-pack/plugins/serverless_search/kibana.jsonc b/x-pack/plugins/serverless_search/kibana.jsonc index fc298a895174e..3a98a87c032a4 100644 --- a/x-pack/plugins/serverless_search/kibana.jsonc +++ b/x-pack/plugins/serverless_search/kibana.jsonc @@ -29,6 +29,7 @@ "optionalPlugins": [ "indexManagement", "searchConnectors", + "searchInferenceEndpoints", "usageCollection" ], "requiredBundles": [ diff --git a/x-pack/plugins/serverless_search/public/navigation_tree.ts b/x-pack/plugins/serverless_search/public/navigation_tree.ts index 5878031d99dc3..d21eee8de9c19 100644 --- a/x-pack/plugins/serverless_search/public/navigation_tree.ts +++ b/x-pack/plugins/serverless_search/public/navigation_tree.ts @@ -93,6 +93,25 @@ export const navigationTree: NavigationTreeDefinition = { }, ], }, + { + id: 'relevance', + title: i18n.translate('xpack.serverlessSearch.nav.relevance', { + defaultMessage: 'Relevance', + }), + spaceBefore: 'm', + children: [ + { + id: 'searchInferenceEndpoints', + title: i18n.translate( + 'xpack.serverlessSearch.nav.relevance.searchInferenceEndpoints', + { + defaultMessage: 'Inference Endpoints', + } + ), + link: 'searchInferenceEndpoints', + }, + ], + }, ], }, ], diff --git a/x-pack/plugins/serverless_search/public/types.ts b/x-pack/plugins/serverless_search/public/types.ts index 5b502eb4f0d9e..e4eab5fbfd61d 100644 --- a/x-pack/plugins/serverless_search/public/types.ts +++ b/x-pack/plugins/serverless_search/public/types.ts @@ -8,6 +8,7 @@ import { CloudSetup, CloudStart } from '@kbn/cloud-plugin/public'; import { ConsolePluginStart } from '@kbn/console-plugin/public'; import { SearchPlaygroundPluginStart } from '@kbn/search-playground/public'; +import { SearchInferenceEndpointsPluginStart } from '@kbn/search-inference-endpoints/public'; import { ManagementSetup, ManagementStart } from '@kbn/management-plugin/public'; import { SecurityPluginStart } from '@kbn/security-plugin/public'; import { ServerlessPluginSetup, ServerlessPluginStart } from '@kbn/serverless/public'; @@ -32,6 +33,7 @@ export interface ServerlessSearchPluginStartDependencies { cloud: CloudStart; console: ConsolePluginStart; searchPlayground: SearchPlaygroundPluginStart; + searchInferenceEndpoints?: SearchInferenceEndpointsPluginStart; management: ManagementStart; security: SecurityPluginStart; serverless: ServerlessPluginStart; diff --git a/x-pack/plugins/serverless_search/tsconfig.json b/x-pack/plugins/serverless_search/tsconfig.json index f7d7fb3444554..8cf2dec121d46 100644 --- a/x-pack/plugins/serverless_search/tsconfig.json +++ b/x-pack/plugins/serverless_search/tsconfig.json @@ -49,5 +49,6 @@ "@kbn/index-management", "@kbn/react-kibana-context-render", "@kbn/search-playground", + "@kbn/search-inference-endpoints", ] } diff --git a/yarn.lock b/yarn.lock index 27e604d8f4b6a..61bd27eecdc9a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6024,6 +6024,10 @@ version "0.0.0" uid "" +"@kbn/search-inference-endpoints@link:x-pack/plugins/search_inference_endpoints": + version "0.0.0" + uid "" + "@kbn/search-notebooks@link:x-pack/plugins/search_notebooks": version "0.0.0" uid ""