diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index 70be71f08184a..2e7f5a815c6ea 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -328,6 +328,7 @@ enabled: - x-pack/test/functional/config_security_basic.ts - x-pack/test/functional/config.ccs.ts - x-pack/test/functional/config.firefox.js + - x-pack/test/functional/config.upgrade_assistant.ts - x-pack/test/functional_cloud/config.ts - x-pack/test/kubernetes_security/basic/config.ts - x-pack/test/licensing_plugin/config.public.ts diff --git a/docs/user/production-considerations/alerting-production-considerations.asciidoc b/docs/user/production-considerations/alerting-production-considerations.asciidoc index e3d343475e175..59c8a4bfa6d15 100644 --- a/docs/user/production-considerations/alerting-production-considerations.asciidoc +++ b/docs/user/production-considerations/alerting-production-considerations.asciidoc @@ -56,14 +56,10 @@ Predicting the buffer required to account for actions depends heavily on the rul experimental[] -Alerts and actions log activity in a set of "event log" indices. These indices are configured with an index lifecycle management (ILM) policy, which you can customize. The default policy rolls over the index when it reaches 50GB, or after 30 days. Indices over 90 days old are deleted. +Alerts and actions log activity in a set of "event log" data streams, one per Kibana version, named `.kibana-event-log-{VERSION}`. These data streams are configured with a lifecycle data retention of 90 days. This can be updated to other values via the standard data stream lifecycle APIs. Note that the event log data contains the data shown in the alerting pages in {kib}, so reducing the data retention period will result in less data being available to view. -The name of the index policy is `kibana-event-log-policy`. {kib} creates the index policy on startup, if it doesn't already exist. The index policy can be customized for your environment, but {kib} never modifies the index policy after creating it. - -Because {kib} uses the documents to display historic data, you should set the delete phase longer than you would like the historic data to be shown. For example, if you would like to see one month's worth of historic data, you should set the delete phase to at least one month. - -For more information on index lifecycle management, see: -{ref}/index-lifecycle-management.html[Index Lifecycle Policies]. +For more information on data stream lifecycle management, see: +{ref}/data-stream-lifecycle.html[Data stream lifecycle]. [float] [[alerting-circuit-breakers]] diff --git a/examples/expressions_explorer/public/actions_and_expressions.tsx b/examples/expressions_explorer/public/actions_and_expressions.tsx index d7322cd34e236..5e118fb4e411c 100644 --- a/examples/expressions_explorer/public/actions_and_expressions.tsx +++ b/examples/expressions_explorer/public/actions_and_expressions.tsx @@ -11,13 +11,14 @@ import { EuiFlexItem, EuiFlexGroup, EuiPageBody, - EuiPageContent_Deprecated as EuiPageContent, - EuiPageContentBody_Deprecated as EuiPageContentBody, + EuiPageTemplate, + EuiPageSection, EuiPageHeader, EuiPageHeaderSection, EuiPanel, EuiText, EuiTitle, + EuiSpacer, } from '@elastic/eui'; import { ExpressionsStart } from '@kbn/expressions-plugin/public'; import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; @@ -55,8 +56,8 @@ export function ActionsExpressionsExample({ expressions, actions }: Props) { - - + + @@ -67,6 +68,8 @@ export function ActionsExpressionsExample({ expressions, actions }: Props) { + + @@ -86,8 +89,8 @@ export function ActionsExpressionsExample({ expressions, actions }: Props) { - - + + ); } diff --git a/examples/expressions_explorer/public/actions_and_expressions2.tsx b/examples/expressions_explorer/public/actions_and_expressions2.tsx index 10a3c7a679195..4887d358ec731 100644 --- a/examples/expressions_explorer/public/actions_and_expressions2.tsx +++ b/examples/expressions_explorer/public/actions_and_expressions2.tsx @@ -11,13 +11,14 @@ import { EuiFlexItem, EuiFlexGroup, EuiPageBody, - EuiPageContent_Deprecated as EuiPageContent, - EuiPageContentBody_Deprecated as EuiPageContentBody, + EuiPageTemplate, + EuiPageSection, EuiPageHeader, EuiPageHeaderSection, EuiPanel, EuiText, EuiTitle, + EuiSpacer, } from '@elastic/eui'; import { ExpressionsStart } from '@kbn/expressions-plugin/public'; import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; @@ -54,8 +55,8 @@ export function ActionsExpressionsExample2({ expressions, actions }: Props) { - - + + @@ -65,6 +66,8 @@ export function ActionsExpressionsExample2({ expressions, actions }: Props) { + + @@ -86,8 +89,8 @@ export function ActionsExpressionsExample2({ expressions, actions }: Props) { - - + + ); } diff --git a/examples/expressions_explorer/public/app.tsx b/examples/expressions_explorer/public/app.tsx index 96e8fcb2908bd..776d612fd395d 100644 --- a/examples/expressions_explorer/public/app.tsx +++ b/examples/expressions_explorer/public/app.tsx @@ -12,8 +12,8 @@ import { EuiPage, EuiPageHeader, EuiPageBody, - EuiPageContent_Deprecated as EuiPageContent, - EuiPageContentBody_Deprecated as EuiPageContentBody, + EuiPageTemplate, + EuiPageSection, EuiSpacer, EuiText, EuiLink, @@ -55,9 +55,11 @@ const ExpressionsExplorer = ({ - Expressions Explorer - - + + + + +

There are a couple of ways to run the expressions. Below some of the options are @@ -87,8 +89,8 @@ const ExpressionsExplorer = ({ - - + + diff --git a/examples/expressions_explorer/public/render_expressions.tsx b/examples/expressions_explorer/public/render_expressions.tsx index 768aaff133686..7901b456232cb 100644 --- a/examples/expressions_explorer/public/render_expressions.tsx +++ b/examples/expressions_explorer/public/render_expressions.tsx @@ -11,14 +11,15 @@ import { EuiFlexItem, EuiFlexGroup, EuiPageBody, - EuiPageContent_Deprecated as EuiPageContent, - EuiPageContentBody_Deprecated as EuiPageContentBody, + EuiPageTemplate, + EuiPageSection, EuiPageHeader, EuiPageHeaderSection, EuiPanel, EuiText, EuiTitle, EuiButton, + EuiSpacer, } from '@elastic/eui'; import { ExpressionsStart } from '@kbn/expressions-plugin/public'; import { Start as InspectorStart } from '@kbn/inspector-plugin/public'; @@ -49,8 +50,8 @@ export function RenderExpressionsExample({ expressions, inspector }: Props) { - - + + @@ -69,6 +70,8 @@ export function RenderExpressionsExample({ expressions, inspector }: Props) { + + @@ -90,8 +93,8 @@ export function RenderExpressionsExample({ expressions, inspector }: Props) { - - + + ); } diff --git a/examples/expressions_explorer/public/run_expressions.tsx b/examples/expressions_explorer/public/run_expressions.tsx index 0b0558568522e..f30069acbc3ca 100644 --- a/examples/expressions_explorer/public/run_expressions.tsx +++ b/examples/expressions_explorer/public/run_expressions.tsx @@ -13,14 +13,15 @@ import { EuiFlexItem, EuiFlexGroup, EuiPageBody, - EuiPageContent_Deprecated as EuiPageContent, - EuiPageContentBody_Deprecated as EuiPageContentBody, + EuiPageTemplate, + EuiPageSection, EuiPageHeader, EuiPageHeaderSection, EuiPanel, EuiText, EuiTitle, EuiButton, + EuiSpacer, } from '@elastic/eui'; import { ExpressionsStart } from '@kbn/expressions-plugin/public'; import { Adapters, Start as InspectorStart } from '@kbn/inspector-plugin/public'; @@ -64,8 +65,8 @@ export function RunExpressionsExample({ expressions, inspector }: Props) { - - + + @@ -84,6 +85,8 @@ export function RunExpressionsExample({ expressions, inspector }: Props) { + + @@ -104,8 +107,8 @@ export function RunExpressionsExample({ expressions, inspector }: Props) { - - + + ); } diff --git a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.test.ts b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.test.ts index ef0c33330a57d..fd1fadff60512 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.test.ts +++ b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.test.ts @@ -34,13 +34,13 @@ describe('breadcrumbs', () => { const currentLocationPathName = '/foo/item1'; const { projectNavigation, history } = setup({ locationPathName: currentLocationPathName }); - projectNavigation.setProjectNavigation({ + const mockNavigation = { navigationTree: [ { id: 'root', title: 'Root', path: ['root'], - breadcrumbStatus: 'hidden', + breadcrumbStatus: 'hidden' as 'hidden', children: [ { id: 'subNav', @@ -64,8 +64,9 @@ describe('breadcrumbs', () => { ], }, ], - }); - return { projectNavigation, history }; + }; + projectNavigation.setProjectNavigation(mockNavigation); + return { projectNavigation, history, mockNavigation }; }; test('should set breadcrumbs home / nav / custom', async () => { @@ -151,6 +152,42 @@ describe('breadcrumbs', () => { breadcrumbs = await firstValueFrom(projectNavigation.getProjectBreadcrumbs$()); expect(breadcrumbs).toHaveLength(1); // only home is left }); + + // this handles race condition where the final `setProjectNavigation` update happens after the app called `setProjectBreadcrumbs` + test("shouldn't reset initial deep context breadcrumbs", async () => { + const { projectNavigation, mockNavigation } = setupWithNavTree(); + projectNavigation.setProjectNavigation({ navigationTree: [] }); // reset simulating initial state + projectNavigation.setProjectBreadcrumbs([ + { text: 'custom1', href: '/custom1' }, + { text: 'custom2', href: '/custom1/custom2' }, + ]); + projectNavigation.setProjectNavigation(mockNavigation); // restore navigation + + const breadcrumbs = await firstValueFrom(projectNavigation.getProjectBreadcrumbs$()); + expect(breadcrumbs).toHaveLength(4); + }); + + test("shouldn't reset custom breadcrumbs when nav node contents changes, but not the path", async () => { + const { projectNavigation, mockNavigation } = setupWithNavTree(); + projectNavigation.setProjectBreadcrumbs([ + { text: 'custom1', href: '/custom1' }, + { text: 'custom2', href: '/custom1/custom2' }, + ]); + let breadcrumbs = await firstValueFrom(projectNavigation.getProjectBreadcrumbs$()); + expect(breadcrumbs).toHaveLength(4); + + // navigation node contents changed, but not the path + projectNavigation.setProjectNavigation({ + navigationTree: [ + { ...mockNavigation.navigationTree[0], title: 'Changed title' }, + ...mockNavigation.navigationTree, + ], + }); + + // context breadcrumbs should not reset + breadcrumbs = await firstValueFrom(projectNavigation.getProjectBreadcrumbs$()); + expect(breadcrumbs).toHaveLength(4); + }); }); describe('getActiveNodes$()', () => { diff --git a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.ts b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.ts index 50bf609dde0da..90be8ff754053 100644 --- a/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.ts +++ b/packages/core/chrome/core-chrome-browser-internal/src/project_navigation/project_navigation_service.ts @@ -16,7 +16,17 @@ import { ChromeProjectNavigationNode, } from '@kbn/core-chrome-browser'; import type { HttpStart } from '@kbn/core-http-browser'; -import { BehaviorSubject, Observable, combineLatest, map, takeUntil, ReplaySubject } from 'rxjs'; +import { + BehaviorSubject, + Observable, + combineLatest, + map, + takeUntil, + ReplaySubject, + skip, + distinctUntilChanged, + skipWhile, +} from 'rxjs'; import type { Location } from 'history'; import deepEqual from 'react-fast-compare'; import classnames from 'classnames'; @@ -55,10 +65,25 @@ export class ProjectNavigationService { this.onHistoryLocationChange(application.history.location); this.unlistenHistory = application.history.listen(this.onHistoryLocationChange.bind(this)); - this.activeNodes$.pipe(takeUntil(this.stop$)).subscribe(() => { - // reset the breadcrumbs when the active nodes change - this.projectBreadcrumbs$.next({ breadcrumbs: [], params: { absolute: false } }); - }); + this.activeNodes$ + .pipe( + takeUntil(this.stop$), + // skip while the project navigation is not set + skipWhile(() => !this.projectNavigation$.getValue()), + // only reset when the active breadcrumb path changes, use ids to get more stable reference + distinctUntilChanged((prevNodes, nextNodes) => + deepEqual( + prevNodes?.[0]?.map((node) => node.id), + nextNodes?.[0]?.map((node) => node.id) + ) + ), + // skip the initial state, we only want to reset the breadcrumbs when the active nodes change + skip(1) + ) + .subscribe(() => { + // reset the breadcrumbs when the active nodes change + this.projectBreadcrumbs$.next({ breadcrumbs: [], params: { absolute: false } }); + }); return { setProjectHome: (homeHref: string) => { diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 249820124ef7a..7bf1b32195dd5 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -319,6 +319,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { overview: `${KIBANA_DOCS}upgrade-assistant.html`, batchReindex: `${KIBANA_DOCS}batch-start-resume-reindex.html`, remoteReindex: `${ELASTICSEARCH_DOCS}docs-reindex.html#reindex-from-remote`, + reindexWithPipeline: `${ELASTICSEARCH_DOCS}docs-reindex.html#reindex-with-an-ingest-pipeline`, }, rollupJobs: `${KIBANA_DOCS}data-rollups.html`, elasticsearch: { diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index dff8af667ccba..5869005187db6 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -298,6 +298,7 @@ export interface DocLinks { readonly overview: string; readonly batchReindex: string; readonly remoteReindex: string; + readonly reindexWithPipeline: string; }; readonly rollupJobs: string; readonly elasticsearch: Record; diff --git a/src/plugins/dashboard/kibana.jsonc b/src/plugins/dashboard/kibana.jsonc index dbf2f49b4857d..c22d68173deb6 100644 --- a/src/plugins/dashboard/kibana.jsonc +++ b/src/plugins/dashboard/kibana.jsonc @@ -33,7 +33,8 @@ "savedObjectsTaggingOss", "screenshotMode", "usageCollection", - "taskManager" + "taskManager", + "serverless" ], "requiredBundles": ["kibanaReact", "kibanaUtils", "presentationUtil"] } diff --git a/src/plugins/dashboard/public/dashboard_app/listing_page/dashboard_listing_page.tsx b/src/plugins/dashboard/public/dashboard_app/listing_page/dashboard_listing_page.tsx index d02a9a31ef667..697a516e1fc6d 100644 --- a/src/plugins/dashboard/public/dashboard_app/listing_page/dashboard_listing_page.tsx +++ b/src/plugins/dashboard/public/dashboard_app/listing_page/dashboard_listing_page.tsx @@ -37,6 +37,7 @@ export const DashboardListingPage = ({ }: DashboardListingPageProps) => { const { data: { query }, + serverless, chrome: { setBreadcrumbs }, dashboardContentManagement: { findDashboards }, } = pluginServices.getServices(); @@ -59,7 +60,13 @@ export const DashboardListingPage = ({ text: getDashboardBreadcrumb(), }, ]); - }, [setBreadcrumbs]); + + if (serverless?.setBreadcrumbs) { + // if serverless breadcrumbs available, + // reset any deeper context breadcrumbs to only keep the main "dashboard" part that comes from the navigation config + serverless.setBreadcrumbs([]); + } + }, [setBreadcrumbs, serverless]); useEffect(() => { // syncs `_g` portion of url with query services diff --git a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_top_nav.tsx index 5e80bdf3ec97a..d6e61f9e1c88b 100644 --- a/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/dashboard_app/top_nav/dashboard_top_nav.tsx @@ -61,6 +61,7 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr getIsVisible$: getChromeIsVisible$, recentlyAccessed: chromeRecentlyAccessed, }, + serverless, settings: { uiSettings }, navigation: { TopNavMenu }, embeddable: { getStateTransfer }, @@ -136,14 +137,7 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr * Set breadcrumbs to dashboard title when dashboard's title or view mode changes */ useEffect(() => { - setBreadcrumbs([ - { - text: getDashboardBreadcrumb(), - 'data-test-subj': 'dashboardListingBreadcrumb', - onClick: () => { - redirectTo({ destination: 'listing' }); - }, - }, + const dashboardTitleBreadcrumbs = [ { text: viewMode === ViewMode.EDIT ? ( @@ -160,8 +154,26 @@ export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavPr } : undefined, }, - ]); - }, [setBreadcrumbs, redirectTo, dashboardTitle, dashboard, viewMode]); + ]; + + if (serverless?.setBreadcrumbs) { + // set serverless breadcrumbs if available, + // set only the dashboardTitleBreadcrumbs because the main breadcrumbs automatically come as part of the navigation config + serverless.setBreadcrumbs(dashboardTitleBreadcrumbs); + } else { + // non-serverless regular breadcrumbs + setBreadcrumbs([ + { + text: getDashboardBreadcrumb(), + 'data-test-subj': 'dashboardListingBreadcrumb', + onClick: () => { + redirectTo({ destination: 'listing' }); + }, + }, + ...dashboardTitleBreadcrumbs, + ]); + } + }, [setBreadcrumbs, redirectTo, dashboardTitle, dashboard, viewMode, serverless]); /** * Build app leave handler whenever hasUnsavedChanges changes diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 41f10fd31d8bb..b42a13f858965 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -51,6 +51,7 @@ import type { import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { UrlForwardingSetup, UrlForwardingStart } from '@kbn/url-forwarding-plugin/public'; import type { SavedObjectTaggingOssPluginStart } from '@kbn/saved-objects-tagging-oss-plugin/public'; +import type { ServerlessPluginStart } from '@kbn/serverless/public'; import { CustomBrandingStart } from '@kbn/core-custom-branding-browser'; import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; @@ -107,6 +108,7 @@ export interface DashboardStartDependencies { usageCollection?: UsageCollectionStart; visualizations: VisualizationsStart; customBranding: CustomBrandingStart; + serverless?: ServerlessPluginStart; } export interface DashboardSetup { diff --git a/src/plugins/dashboard/public/services/plugin_services.stub.ts b/src/plugins/dashboard/public/services/plugin_services.stub.ts index 3be6fe599cba6..0ae4159ed2128 100644 --- a/src/plugins/dashboard/public/services/plugin_services.stub.ts +++ b/src/plugins/dashboard/public/services/plugin_services.stub.ts @@ -41,6 +41,7 @@ import { dashboardContentManagementServiceFactory } from './dashboard_content_ma import { customBrandingServiceFactory } from './custom_branding/custom_branding.stub'; import { savedObjectsManagementServiceFactory } from './saved_objects_management/saved_objects_management_service.stub'; import { contentManagementServiceFactory } from './content_management/content_management_service.stub'; +import { serverlessServiceFactory } from './serverless/serverless_service.stub'; export const providers: PluginServiceProviders = { dashboardContentManagement: new PluginServiceProvider(dashboardContentManagementServiceFactory), @@ -70,6 +71,7 @@ export const providers: PluginServiceProviders = { customBranding: new PluginServiceProvider(customBrandingServiceFactory), savedObjectsManagement: new PluginServiceProvider(savedObjectsManagementServiceFactory), contentManagement: new PluginServiceProvider(contentManagementServiceFactory), + serverless: new PluginServiceProvider(serverlessServiceFactory), }; export const registry = new PluginServiceRegistry(providers); diff --git a/src/plugins/dashboard/public/services/plugin_services.ts b/src/plugins/dashboard/public/services/plugin_services.ts index c9e69481b2d69..d84b55d0ff4a1 100644 --- a/src/plugins/dashboard/public/services/plugin_services.ts +++ b/src/plugins/dashboard/public/services/plugin_services.ts @@ -42,6 +42,7 @@ import { customBrandingServiceFactory } from './custom_branding/custom_branding_ import { savedObjectsManagementServiceFactory } from './saved_objects_management/saved_objects_management_service'; import { dashboardContentManagementServiceFactory } from './dashboard_content_management/dashboard_content_management_service'; import { contentManagementServiceFactory } from './content_management/content_management_service'; +import { serverlessServiceFactory } from './serverless/serverless_service'; const providers: PluginServiceProviders = { dashboardContentManagement: new PluginServiceProvider(dashboardContentManagementServiceFactory, [ @@ -84,6 +85,7 @@ const providers: PluginServiceProviders(); diff --git a/src/plugins/dashboard/public/services/serverless/serverless_service.stub.ts b/src/plugins/dashboard/public/services/serverless/serverless_service.stub.ts new file mode 100644 index 0000000000000..d9af53f1ddc0d --- /dev/null +++ b/src/plugins/dashboard/public/services/serverless/serverless_service.stub.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; +import { DashboardServerlessService } from './types'; + +export type ServerlessServiceFactory = PluginServiceFactory; + +export const serverlessServiceFactory: ServerlessServiceFactory = () => { + return {}; +}; diff --git a/src/plugins/dashboard/public/services/serverless/serverless_service.ts b/src/plugins/dashboard/public/services/serverless/serverless_service.ts new file mode 100644 index 0000000000000..c22eea4560183 --- /dev/null +++ b/src/plugins/dashboard/public/services/serverless/serverless_service.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; +import { DashboardStartDependencies } from '../../plugin'; +import { DashboardServerlessService } from './types'; + +export type ServerlessServiceFactory = KibanaPluginServiceFactory< + DashboardServerlessService, + DashboardStartDependencies +>; + +export const serverlessServiceFactory: ServerlessServiceFactory = ({ startPlugins }) => { + const { serverless } = startPlugins; + + return { setBreadcrumbs: serverless?.setBreadcrumbs }; +}; diff --git a/src/plugins/dashboard/public/services/serverless/types.ts b/src/plugins/dashboard/public/services/serverless/types.ts new file mode 100644 index 0000000000000..ce12102fa87c3 --- /dev/null +++ b/src/plugins/dashboard/public/services/serverless/types.ts @@ -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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { ServerlessPluginStart } from '@kbn/serverless/public'; + +export interface DashboardServerlessService { + setBreadcrumbs?: ServerlessPluginStart['setBreadcrumbs']; +} diff --git a/src/plugins/dashboard/public/services/types.ts b/src/plugins/dashboard/public/services/types.ts index 27f980ef19b58..13adaf6098070 100644 --- a/src/plugins/dashboard/public/services/types.ts +++ b/src/plugins/dashboard/public/services/types.ts @@ -37,6 +37,7 @@ import { DashboardSpacesService } from './spaces/types'; import { DashboardUrlForwardingService } from './url_forwarding/types'; import { DashboardUsageCollectionService } from './usage_collection/types'; import { DashboardVisualizationsService } from './visualizations/types'; +import { DashboardServerlessService } from './serverless/types'; export type DashboardPluginServiceParams = KibanaPluginServiceParams & { initContext: PluginInitializerContext; // need a custom type so that initContext is a required parameter for initializerContext @@ -70,4 +71,5 @@ export interface DashboardServices { customBranding: DashboardCustomBrandingService; savedObjectsManagement: SavedObjectsManagementPluginStart; contentManagement: ContentManagementPublicStart; + serverless: DashboardServerlessService; // TODO: make this optional in follow up } diff --git a/src/plugins/dashboard/tsconfig.json b/src/plugins/dashboard/tsconfig.json index 6fd24d4e1fb01..9d5dafa47c88a 100644 --- a/src/plugins/dashboard/tsconfig.json +++ b/src/plugins/dashboard/tsconfig.json @@ -63,7 +63,8 @@ "@kbn/content-management-table-list-view", "@kbn/content-management-table-list-view-table", "@kbn/shared-ux-prompt-not-found", - "@kbn/content-management-content-editor" + "@kbn/content-management-content-editor", + "@kbn/serverless" ], "exclude": ["target/**/*"] } diff --git a/src/plugins/data/kibana.jsonc b/src/plugins/data/kibana.jsonc index 90658599a87bf..2881da532d63a 100644 --- a/src/plugins/data/kibana.jsonc +++ b/src/plugins/data/kibana.jsonc @@ -28,7 +28,6 @@ ], "optionalPlugins": [ "usageCollection", - "taskManager", "security" ], "requiredBundles": [ diff --git a/src/plugins/data/server/plugin.ts b/src/plugins/data/server/plugin.ts index 70fa72b1634cb..1b2b0c3c78ca1 100644 --- a/src/plugins/data/server/plugin.ts +++ b/src/plugins/data/server/plugin.ts @@ -12,10 +12,6 @@ import { BfetchServerSetup } from '@kbn/bfetch-plugin/server'; import { PluginStart as DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import { FieldFormatsSetup, FieldFormatsStart } from '@kbn/field-formats-plugin/server'; -import type { - TaskManagerSetupContract, - TaskManagerStartContract, -} from '@kbn/task-manager-plugin/server'; import type { SecurityPluginSetup } from '@kbn/security-plugin/server'; import { ConfigSchema } from '../config'; import type { ISearchSetup, ISearchStart } from './search'; @@ -55,7 +51,6 @@ export interface DataPluginSetupDependencies { expressions: ExpressionsServerSetup; usageCollection?: UsageCollectionSetup; fieldFormats: FieldFormatsSetup; - taskManager?: TaskManagerSetupContract; security?: SecurityPluginSetup; } @@ -63,7 +58,6 @@ export interface DataPluginStartDependencies { fieldFormats: FieldFormatsStart; logger: Logger; dataViews: DataViewsServerPluginStart; - taskManager?: TaskManagerStartContract; } export class DataServerPlugin @@ -90,14 +84,7 @@ export class DataServerPlugin public setup( core: CoreSetup, - { - bfetch, - expressions, - usageCollection, - fieldFormats, - taskManager, - security, - }: DataPluginSetupDependencies + { bfetch, expressions, usageCollection, fieldFormats, security }: DataPluginSetupDependencies ) { this.scriptsService.setup(core); const querySetup = this.queryService.setup(core); @@ -110,7 +97,6 @@ export class DataServerPlugin expressions, usageCollection, security, - taskManager, }); return { @@ -120,14 +106,10 @@ export class DataServerPlugin }; } - public start( - core: CoreStart, - { fieldFormats, dataViews, taskManager }: DataPluginStartDependencies - ) { + public start(core: CoreStart, { fieldFormats, dataViews }: DataPluginStartDependencies) { const search = this.searchService.start(core, { fieldFormats, indexPatterns: dataViews, - taskManager, }); const datatableUtilities = new DatatableUtilitiesService( search.aggs, diff --git a/src/plugins/data/server/search/mocks.ts b/src/plugins/data/server/search/mocks.ts index 8630f2be1585c..118d43ed14d25 100644 --- a/src/plugins/data/server/search/mocks.ts +++ b/src/plugins/data/server/search/mocks.ts @@ -17,6 +17,7 @@ export function createSearchSetupMock(): jest.Mocked { aggs: searchAggsSetupMock(), registerSearchStrategy: jest.fn(), searchSource: searchSourceMock.createSetupContract(), + enableRollups: jest.fn(), }; } diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index f9d1c79390fb5..0387cb820f165 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -25,10 +25,6 @@ import { ExpressionsServerSetup } from '@kbn/expressions-plugin/server'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/server'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server'; import { KbnServerError } from '@kbn/kibana-utils-plugin/server'; -import type { - TaskManagerSetupContract, - TaskManagerStartContract, -} from '@kbn/task-manager-plugin/server'; import type { SecurityPluginSetup } from '@kbn/security-plugin/server'; import type { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server'; import type { @@ -107,7 +103,6 @@ export interface SearchServiceSetupDependencies { bfetch: BfetchServerSetup; expressions: ExpressionsServerSetup; usageCollection?: UsageCollectionSetup; - taskManager?: TaskManagerSetupContract; security?: SecurityPluginSetup; } @@ -115,7 +110,6 @@ export interface SearchServiceSetupDependencies { export interface SearchServiceStartDependencies { fieldFormats: FieldFormatsStart; indexPatterns: DataViewsServerPluginStart; - taskManager?: TaskManagerStartContract; } /** @internal */ @@ -131,6 +125,7 @@ export class SearchService implements Plugin { private sessionService: SearchSessionService; private asScoped!: ISearchStart['asScoped']; private searchAsInternalUser!: ISearchStrategy; + private rollupsEnabled: boolean = false; constructor( private initializerContext: PluginInitializerContext, @@ -145,7 +140,7 @@ export class SearchService implements Plugin { public setup( core: CoreSetup, - { bfetch, expressions, usageCollection, taskManager, security }: SearchServiceSetupDependencies + { bfetch, expressions, usageCollection, security }: SearchServiceSetupDependencies ): ISearchSetup { core.savedObjects.registerType(searchSessionSavedObjectType); const usage = usageCollection ? usageProvider(core) : undefined; @@ -261,12 +256,13 @@ export class SearchService implements Plugin { registerSearchStrategy: this.registerSearchStrategy, usage, searchSource: this.searchSourceService.setup(), + enableRollups: () => (this.rollupsEnabled = true), }; } public start( core: CoreStart, - { fieldFormats, indexPatterns, taskManager }: SearchServiceStartDependencies + { fieldFormats, indexPatterns }: SearchServiceStartDependencies ): ISearchStart { const { elasticsearch, savedObjects, uiSettings } = core; @@ -278,7 +274,7 @@ export class SearchService implements Plugin { indexPatterns, }); - this.asScoped = this.asScopedProvider(core); + this.asScoped = this.asScopedProvider(core, this.rollupsEnabled); return { aggs, searchAsInternalUser: this.searchAsInternalUser, @@ -516,7 +512,7 @@ export class SearchService implements Plugin { return deps.searchSessionsClient.extend(sessionId, expires); }; - private asScopedProvider = (core: CoreStart) => { + private asScopedProvider = (core: CoreStart, rollupsEnabled: boolean = false) => { const { elasticsearch, savedObjects, uiSettings } = core; const getSessionAsScoped = this.sessionService.asScopedProvider(core); return (request: KibanaRequest): IScopedSearchClient => { @@ -530,6 +526,7 @@ export class SearchService implements Plugin { uiSettings.asScopedToClient(savedObjectsClient) ), request, + rollupsEnabled, }; return { search: < diff --git a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.test.ts b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.test.ts index 070d07c07c956..627bb5fe29293 100644 --- a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.test.ts +++ b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.test.ts @@ -64,6 +64,7 @@ describe('ES search strategy', () => { }, }, searchSessionsClient: createSearchSessionsClientMock(), + rollupsEnabled: true, } as unknown as SearchStrategyDependencies; const mockLegacyConfig$ = new BehaviorSubject({ elasticsearch: { @@ -233,6 +234,31 @@ describe('ES search strategy', () => { expect(method).toBe('POST'); expect(path).toBe('/foo-%E7%A8%8B/_rollup_search'); }); + + it("doesn't call the rollup API if the index is a rollup type BUT rollups are disabled", async () => { + mockApiCaller.mockResolvedValueOnce(mockRollupResponse); + mockSubmitCaller.mockResolvedValueOnce(mockAsyncResponse); + + const params = { index: 'foo-程', body: { query: {} } }; + const esSearch = await enhancedEsSearchStrategyProvider( + mockLegacyConfig$, + mockSearchConfig, + mockLogger + ); + + await esSearch + .search( + { + indexType: 'rollup', + params, + }, + {}, + { ...mockDeps, rollupsEnabled: false } + ) + .toPromise(); + + expect(mockApiCaller).toBeCalledTimes(0); + }); }); describe('with sessionId', () => { diff --git a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts index e5567b45f1e06..298933907b8bb 100644 --- a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts +++ b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts @@ -154,7 +154,7 @@ export const enhancedEsSearchStrategyProvider = ( throw new KbnServerError('Unknown indexType', 400); } - if (request.indexType === undefined) { + if (request.indexType === undefined || !deps.rollupsEnabled) { return asyncSearch(request, options, deps); } else { return from(rollupSearch(request, options, deps)); diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts index 50fc29334d22c..8b94085c3f80a 100644 --- a/src/plugins/data/server/search/types.ts +++ b/src/plugins/data/server/search/types.ts @@ -35,6 +35,7 @@ export interface SearchStrategyDependencies { uiSettingsClient: Pick; searchSessionsClient: IScopedSearchSessionsClient; request: KibanaRequest; + rollupsEnabled?: boolean; } export interface ISearchSetup { @@ -55,7 +56,7 @@ export interface ISearchSetup { * Used internally for telemetry */ usage?: SearchUsage; - + enableRollups: () => void; searchSource: ReturnType; } diff --git a/src/plugins/data/tsconfig.json b/src/plugins/data/tsconfig.json index 450c858265917..73eb71508a895 100644 --- a/src/plugins/data/tsconfig.json +++ b/src/plugins/data/tsconfig.json @@ -25,7 +25,6 @@ "@kbn/field-formats-plugin", "@kbn/data-views-plugin", "@kbn/screenshot-mode-plugin", - "@kbn/task-manager-plugin", "@kbn/security-plugin", "@kbn/expressions-plugin", "@kbn/field-types", diff --git a/src/plugins/data_views/server/data_views_service_factory.ts b/src/plugins/data_views/server/data_views_service_factory.ts index fb5ae2c5afe3a..ac27ad0bc8093 100644 --- a/src/plugins/data_views/server/data_views_service_factory.ts +++ b/src/plugins/data_views/server/data_views_service_factory.ts @@ -25,6 +25,7 @@ interface DataViewsServiceFactoryDeps { uiSettings: UiSettingsServiceStart; fieldFormats: FieldFormatsStart; capabilities: CoreStart['capabilities']; + rollupsEnabled: boolean; } /** @@ -38,14 +39,18 @@ export const dataViewsServiceFactory = (deps: DataViewsServiceFactoryDeps) => request?: KibanaRequest, byPassCapabilities?: boolean ) { - const { logger, uiSettings, fieldFormats, capabilities } = deps; + const { logger, uiSettings, fieldFormats, capabilities, rollupsEnabled } = deps; const uiSettingsClient = uiSettings.asScopedToClient(savedObjectsClient); const formats = await fieldFormats.fieldFormatServiceFactory(uiSettingsClient); return new DataViewsService({ uiSettings: new UiSettingsServerToCommon(uiSettingsClient), savedObjectsClient: new SavedObjectsClientWrapper(savedObjectsClient), - apiClient: new IndexPatternsApiServer(elasticsearchClient, savedObjectsClient), + apiClient: new IndexPatternsApiServer( + elasticsearchClient, + savedObjectsClient, + rollupsEnabled + ), fieldFormats: formats, onError: (error) => { logger.error(error); diff --git a/src/plugins/data_views/server/fetcher/index_patterns_fetcher.test.ts b/src/plugins/data_views/server/fetcher/index_patterns_fetcher.test.ts index f6f2d378fef7c..6253c68d84fba 100644 --- a/src/plugins/data_views/server/fetcher/index_patterns_fetcher.test.ts +++ b/src/plugins/data_views/server/fetcher/index_patterns_fetcher.test.ts @@ -10,6 +10,19 @@ import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { IndexPatternsFetcher } from '.'; import { elasticsearchServiceMock } from '@kbn/core/server/mocks'; +const rollupResponse = { + foo: { + rollup_jobs: [ + { + index_pattern: 'foo', + job_id: '123', + rollup_index: 'foo', + fields: [], + }, + ], + }, +}; + describe('Index Pattern Fetcher - server', () => { let indexPatterns: IndexPatternsFetcher; let esClient: ReturnType; @@ -21,12 +34,40 @@ describe('Index Pattern Fetcher - server', () => { beforeEach(() => { jest.clearAllMocks(); esClient = elasticsearchServiceMock.createElasticsearchClient(); - indexPatterns = new IndexPatternsFetcher(esClient); + indexPatterns = new IndexPatternsFetcher(esClient, false, true); }); it('calls fieldcaps once', async () => { esClient.fieldCaps.mockResponse(response as unknown as estypes.FieldCapsResponse); - indexPatterns = new IndexPatternsFetcher(esClient, true); + indexPatterns = new IndexPatternsFetcher(esClient, true, true); await indexPatterns.getFieldsForWildcard({ pattern: patternList }); expect(esClient.fieldCaps).toHaveBeenCalledTimes(1); }); + + it('calls rollup api when given rollup data view', async () => { + esClient.fieldCaps.mockResponse(response as unknown as estypes.FieldCapsResponse); + esClient.rollup.getRollupIndexCaps.mockResponse( + rollupResponse as unknown as estypes.RollupGetRollupIndexCapsResponse + ); + indexPatterns = new IndexPatternsFetcher(esClient, true, true); + await indexPatterns.getFieldsForWildcard({ + pattern: patternList, + type: 'rollup', + rollupIndex: 'foo', + }); + expect(esClient.rollup.getRollupIndexCaps).toHaveBeenCalledTimes(1); + }); + + it("doesn't call rollup api when given rollup data view and rollups are disabled", async () => { + esClient.fieldCaps.mockResponse(response as unknown as estypes.FieldCapsResponse); + esClient.rollup.getRollupIndexCaps.mockResponse( + rollupResponse as unknown as estypes.RollupGetRollupIndexCapsResponse + ); + indexPatterns = new IndexPatternsFetcher(esClient, true, false); + await indexPatterns.getFieldsForWildcard({ + pattern: patternList, + type: 'rollup', + rollupIndex: 'foo', + }); + expect(esClient.rollup.getRollupIndexCaps).toHaveBeenCalledTimes(0); + }); }); diff --git a/src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts b/src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts index 3511cefd34d87..b1d22dc5523c8 100644 --- a/src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts +++ b/src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts @@ -40,10 +40,16 @@ interface FieldSubType { export class IndexPatternsFetcher { private elasticsearchClient: ElasticsearchClient; private allowNoIndices: boolean; + private rollupsEnabled: boolean; - constructor(elasticsearchClient: ElasticsearchClient, allowNoIndices: boolean = false) { + constructor( + elasticsearchClient: ElasticsearchClient, + allowNoIndices: boolean = false, + rollupsEnabled: boolean = false + ) { this.elasticsearchClient = elasticsearchClient; this.allowNoIndices = allowNoIndices; + this.rollupsEnabled = rollupsEnabled; } /** @@ -81,7 +87,7 @@ export class IndexPatternsFetcher { fields: options.fields || ['*'], }); - if (type === 'rollup' && rollupIndex) { + if (this.rollupsEnabled && type === 'rollup' && rollupIndex) { const rollupFields: FieldDescriptor[] = []; const capabilityCheck = getCapabilitiesForRollupIndices( await this.elasticsearchClient.rollup.getRollupIndexCaps({ diff --git a/src/plugins/data_views/server/index.ts b/src/plugins/data_views/server/index.ts index 553f1a48d1d6d..33e93df3be894 100644 --- a/src/plugins/data_views/server/index.ts +++ b/src/plugins/data_views/server/index.ts @@ -10,6 +10,7 @@ export { getFieldByName, findIndexPatternById } from './utils'; export type { FieldDescriptor, RollupIndexCapability } from './fetcher'; export { IndexPatternsFetcher, getCapabilitiesForRollupIndices } from './fetcher'; export type { + DataViewsServerPluginSetup, DataViewsServerPluginStart, DataViewsServerPluginSetupDependencies, DataViewsServerPluginStartDependencies, diff --git a/src/plugins/data_views/server/index_patterns_api_client.ts b/src/plugins/data_views/server/index_patterns_api_client.ts index f470e7f8ed7df..0beb1efacf9b2 100644 --- a/src/plugins/data_views/server/index_patterns_api_client.ts +++ b/src/plugins/data_views/server/index_patterns_api_client.ts @@ -16,7 +16,8 @@ export class IndexPatternsApiServer implements IDataViewsApiClient { esClient: ElasticsearchClient; constructor( elasticsearchClient: ElasticsearchClient, - private readonly savedObjectsClient: SavedObjectsClientContract + private readonly savedObjectsClient: SavedObjectsClientContract, + private readonly rollupsEnabled: boolean ) { this.esClient = elasticsearchClient; } @@ -29,7 +30,11 @@ export class IndexPatternsApiServer implements IDataViewsApiClient { indexFilter, fields, }: GetFieldsOptions) { - const indexPatterns = new IndexPatternsFetcher(this.esClient, allowNoIndex); + const indexPatterns = new IndexPatternsFetcher( + this.esClient, + allowNoIndex, + this.rollupsEnabled + ); return await indexPatterns .getFieldsForWildcard({ pattern, diff --git a/src/plugins/data_views/server/plugin.ts b/src/plugins/data_views/server/plugin.ts index fab72338bdb33..c96de2e4294f0 100644 --- a/src/plugins/data_views/server/plugin.ts +++ b/src/plugins/data_views/server/plugin.ts @@ -33,6 +33,7 @@ export class DataViewsServerPlugin > { private readonly logger: Logger; + private rollupsEnabled: boolean = false; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get('dataView'); @@ -46,7 +47,12 @@ export class DataViewsServerPlugin core.capabilities.registerProvider(capabilitiesProvider); const dataViewRestCounter = usageCollection?.createUsageCounter('dataViewsRestApi'); - registerRoutes(core.http, core.getStartServices, dataViewRestCounter); + registerRoutes( + core.http, + core.getStartServices, + () => this.rollupsEnabled, + dataViewRestCounter + ); expressions.registerFunction(getIndexPatternLoad({ getStartServices: core.getStartServices })); registerIndexPatternsUsageCollector(core.getStartServices, usageCollection); @@ -60,7 +66,9 @@ export class DataViewsServerPlugin }, }); - return {}; + return { + enableRollups: () => (this.rollupsEnabled = true), + }; } public start( @@ -72,6 +80,7 @@ export class DataViewsServerPlugin uiSettings, fieldFormats, capabilities, + rollupsEnabled: this.rollupsEnabled, }); return { diff --git a/src/plugins/data_views/server/rest_api_routes/internal/fields_for.ts b/src/plugins/data_views/server/rest_api_routes/internal/fields_for.ts index 9951bedb82294..15d761935c0a7 100644 --- a/src/plugins/data_views/server/rest_api_routes/internal/fields_for.ts +++ b/src/plugins/data_views/server/rest_api_routes/internal/fields_for.ts @@ -111,86 +111,93 @@ const validate: FullValidationConfig = { }, }; -const handler: RequestHandler<{}, IQuery, IBody> = async (context, request, response) => { - const { asCurrentUser } = (await context.core).elasticsearch.client; - const indexPatterns = new IndexPatternsFetcher(asCurrentUser); - const { - pattern, - meta_fields: metaFields, - type, - rollup_index: rollupIndex, - allow_no_index: allowNoIndex, - include_unmapped: includeUnmapped, - } = request.query; - - // not available to get request - const indexFilter = request.body?.index_filter; - - let parsedFields: string[] = []; - let parsedMetaFields: string[] = []; - try { - parsedMetaFields = parseFields(metaFields); - parsedFields = parseFields(request.query.fields ?? []); - } catch (error) { - return response.badRequest(); - } - - try { - const { fields, indices } = await indexPatterns.getFieldsForWildcard({ +const handler: (isRollupsEnabled: () => boolean) => RequestHandler<{}, IQuery, IBody> = + (isRollupsEnabled) => async (context, request, response) => { + const { asCurrentUser } = (await context.core).elasticsearch.client; + const indexPatterns = new IndexPatternsFetcher(asCurrentUser, undefined, isRollupsEnabled()); + const { pattern, - metaFields: parsedMetaFields, + meta_fields: metaFields, type, - rollupIndex, - fieldCapsOptions: { - allow_no_indices: allowNoIndex || false, - includeUnmapped, - }, - indexFilter, - ...(parsedFields.length > 0 ? { fields: parsedFields } : {}), - }); - - const body: { fields: FieldDescriptorRestResponse[]; indices: string[] } = { fields, indices }; - - return response.ok({ - body, - headers: { - 'content-type': 'application/json', - }, - }); - } catch (error) { - if ( - typeof error === 'object' && - !!error?.isBoom && - !!error?.output?.payload && - typeof error?.output?.payload === 'object' - ) { - const payload = error?.output?.payload; - return response.notFound({ - body: { - message: payload.message, - attributes: payload, + rollup_index: rollupIndex, + allow_no_index: allowNoIndex, + include_unmapped: includeUnmapped, + } = request.query; + + // not available to get request + const indexFilter = request.body?.index_filter; + + let parsedFields: string[] = []; + let parsedMetaFields: string[] = []; + try { + parsedMetaFields = parseFields(metaFields); + parsedFields = parseFields(request.query.fields ?? []); + } catch (error) { + return response.badRequest(); + } + + try { + const { fields, indices } = await indexPatterns.getFieldsForWildcard({ + pattern, + metaFields: parsedMetaFields, + type, + rollupIndex, + fieldCapsOptions: { + allow_no_indices: allowNoIndex || false, + includeUnmapped, + }, + indexFilter, + ...(parsedFields.length > 0 ? { fields: parsedFields } : {}), + }); + + const body: { fields: FieldDescriptorRestResponse[]; indices: string[] } = { + fields, + indices, + }; + + return response.ok({ + body, + headers: { + 'content-type': 'application/json', }, }); - } else { - return response.notFound(); + } catch (error) { + if ( + typeof error === 'object' && + !!error?.isBoom && + !!error?.output?.payload && + typeof error?.output?.payload === 'object' + ) { + const payload = error?.output?.payload; + return response.notFound({ + body: { + message: payload.message, + attributes: payload, + }, + }); + } else { + return response.notFound(); + } } - } -}; + }; -export const registerFieldForWildcard = ( +export const registerFieldForWildcard = async ( router: IRouter, getStartServices: StartServicesAccessor< DataViewsServerPluginStartDependencies, DataViewsServerPluginStart - > + >, + isRollupsEnabled: () => boolean ) => { + const configuredHandler = handler(isRollupsEnabled); + // handler - router.versioned.put({ path, access }).addVersion({ version, validate }, handler); - router.versioned.post({ path, access }).addVersion({ version, validate }, handler); + router.versioned.put({ path, access }).addVersion({ version, validate }, configuredHandler); + router.versioned.post({ path, access }).addVersion({ version, validate }, configuredHandler); router.versioned .get({ path, access }) .addVersion( { version, validate: { request: { query: querySchema }, response: validate.response } }, - handler + configuredHandler ); }; diff --git a/src/plugins/data_views/server/routes.ts b/src/plugins/data_views/server/routes.ts index ef0f342ca4fcc..9c1da30bc99e7 100644 --- a/src/plugins/data_views/server/routes.ts +++ b/src/plugins/data_views/server/routes.ts @@ -20,12 +20,13 @@ export function registerRoutes( DataViewsServerPluginStartDependencies, DataViewsServerPluginStart >, + isRollupsEnabled: () => boolean, dataViewRestCounter?: UsageCounter ) { const router = http.createRouter(); routes.forEach((route) => route(router, getStartServices, dataViewRestCounter)); - registerFieldForWildcard(router, getStartServices); + registerFieldForWildcard(router, getStartServices, isRollupsEnabled); registerHasDataViewsRoute(router); } diff --git a/src/plugins/data_views/server/types.ts b/src/plugins/data_views/server/types.ts index 5b87573322379..d595f6e04b275 100644 --- a/src/plugins/data_views/server/types.ts +++ b/src/plugins/data_views/server/types.ts @@ -53,8 +53,9 @@ export interface DataViewsServerPluginStart { /** * DataViews server plugin setup api */ -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface DataViewsServerPluginSetup {} +export interface DataViewsServerPluginSetup { + enableRollups: () => void; +} /** * Data Views server setup dependencies diff --git a/test/functional/apps/visualize/group5/_tsvb_time_series.ts b/test/functional/apps/visualize/group5/_tsvb_time_series.ts index 823276f0b21c8..28aa95ad24263 100644 --- a/test/functional/apps/visualize/group5/_tsvb_time_series.ts +++ b/test/functional/apps/visualize/group5/_tsvb_time_series.ts @@ -23,7 +23,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const browser = getService('browser'); const kibanaServer = getService('kibanaServer'); - describe('visual builder', function describeIndexTests() { + // Failing: See https://github.com/elastic/kibana/issues/162995 + describe.skip('visual builder', function describeIndexTests() { before(async () => { await security.testUser.setRoles([ 'kibana_admin', diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/deploy_model.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/deploy_model.test.tsx new file mode 100644 index 0000000000000..356de3acc9dc6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/deploy_model.test.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 { setMockValues } from '../../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiButton } from '@elastic/eui'; + +import { DeployModel } from './deploy_model'; +import { TextExpansionDismissButton } from './text_expansion_callout'; + +const DEFAULT_VALUES = { + startTextExpansionModelError: undefined, + isCreateButtonDisabled: false, + isModelDownloadInProgress: false, + isModelDownloaded: false, + isModelStarted: false, + isStartButtonDisabled: false, +}; + +describe('DeployModel', () => { + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(DEFAULT_VALUES); + }); + it('renders deploy button', () => { + const wrapper = shallow( + {}} + ingestionMethod="crawler" + isCreateButtonDisabled={false} + isDismissable={false} + /> + ); + expect(wrapper.find(EuiButton).length).toBe(1); + const button = wrapper.find(EuiButton); + expect(button.prop('disabled')).toBe(false); + }); + it('renders disabled deploy button if it is set to disabled', () => { + const wrapper = shallow( + {}} + ingestionMethod="crawler" + isCreateButtonDisabled + isDismissable={false} + /> + ); + expect(wrapper.find(EuiButton).length).toBe(1); + const button = wrapper.find(EuiButton); + expect(button.prop('disabled')).toBe(true); + }); + it('renders dismiss button if it is set to dismissable', () => { + const wrapper = shallow( + {}} + ingestionMethod="crawler" + isCreateButtonDisabled={false} + isDismissable + /> + ); + expect(wrapper.find(TextExpansionDismissButton).length).toBe(1); + }); + it('does not render dismiss button if it is set to non-dismissable', () => { + const wrapper = shallow( + {}} + ingestionMethod="crawler" + isCreateButtonDisabled={false} + isDismissable={false} + /> + ); + expect(wrapper.find(TextExpansionDismissButton).length).toBe(0); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/deploy_model.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/deploy_model.tsx new file mode 100644 index 0000000000000..40a0059b55eae --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/deploy_model.tsx @@ -0,0 +1,119 @@ +/* + * 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 { useActions } from 'kea'; + +import { + EuiBadge, + EuiButton, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiText, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage, FormattedHTMLMessage } from '@kbn/i18n-react'; + +import { docLinks } from '../../../../../shared/doc_links'; + +import { TextExpansionCallOutState, TextExpansionDismissButton } from './text_expansion_callout'; +import { TextExpansionCalloutLogic } from './text_expansion_callout_logic'; + +export const DeployModel = ({ + dismiss, + ingestionMethod, + isCreateButtonDisabled, + isDismissable, +}: Pick< + TextExpansionCallOutState, + 'dismiss' | 'ingestionMethod' | 'isCreateButtonDisabled' | 'isDismissable' +>) => { + const { createTextExpansionModel } = useActions(TextExpansionCalloutLogic); + + return ( + + + + + + + + + + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.content.index.pipelines.textExpansionCallOut.title', + { defaultMessage: 'Improve your results with ELSER' } + )} +

+
+ + {isDismissable && ( + + + + )} + + + + + + + + + + + + + createTextExpansionModel(undefined)} + > + {i18n.translate( + 'xpack.enterpriseSearch.content.indices.pipelines.textExpansionCallOut.deployButton.label', + { + defaultMessage: 'Deploy', + } + )} + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_deployed.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_deployed.test.tsx new file mode 100644 index 0000000000000..a17eae3ef75f6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_deployed.test.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 { setMockValues } from '../../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiButton } from '@elastic/eui'; + +import { ModelDeployed } from './model_deployed'; +import { TextExpansionDismissButton } from './text_expansion_callout'; + +const DEFAULT_VALUES = { + startTextExpansionModelError: undefined, + isCreateButtonDisabled: false, + isModelDownloadInProgress: false, + isModelDownloaded: false, + isModelStarted: false, + isStartButtonDisabled: false, +}; + +describe('ModelDeployed', () => { + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(DEFAULT_VALUES); + }); + it('renders start button', () => { + const wrapper = shallow( + {}} + ingestionMethod="crawler" + isDismissable={false} + isStartButtonDisabled={false} + /> + ); + expect(wrapper.find(EuiButton).length).toBe(1); + const button = wrapper.find(EuiButton); + expect(button.prop('disabled')).toBe(false); + }); + it('renders disabled start button if it is set to disabled', () => { + const wrapper = shallow( + {}} + ingestionMethod="crawler" + isDismissable={false} + isStartButtonDisabled + /> + ); + expect(wrapper.find(EuiButton).length).toBe(1); + const button = wrapper.find(EuiButton); + expect(button.prop('disabled')).toBe(true); + }); + it('renders dismiss button if it is set to dismissable', () => { + const wrapper = shallow( + {}} + ingestionMethod="crawler" + isDismissable + isStartButtonDisabled={false} + /> + ); + expect(wrapper.find(TextExpansionDismissButton).length).toBe(1); + }); + it('does not render dismiss button if it is set to non-dismissable', () => { + const wrapper = shallow( + {}} + ingestionMethod="crawler" + isDismissable={false} + isStartButtonDisabled={false} + /> + ); + expect(wrapper.find(TextExpansionDismissButton).length).toBe(0); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_deployed.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_deployed.tsx new file mode 100644 index 0000000000000..8c0694cbb8405 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_deployed.tsx @@ -0,0 +1,113 @@ +/* + * 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 { useActions } from 'kea'; + +import { + EuiButton, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiIcon, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { + TextExpansionCallOutState, + TextExpansionDismissButton, + FineTuneModelsButton, +} from './text_expansion_callout'; +import { TextExpansionCalloutLogic } from './text_expansion_callout_logic'; + +export const ModelDeployed = ({ + dismiss, + ingestionMethod, + isDismissable, + isStartButtonDisabled, +}: Pick< + TextExpansionCallOutState, + 'dismiss' | 'ingestionMethod' | 'isDismissable' | 'isStartButtonDisabled' +>) => { + const { startTextExpansionModel } = useActions(TextExpansionCalloutLogic); + + return ( + + + + + + + + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.content.index.pipelines.textExpansionCallOut.deployedTitle', + { defaultMessage: 'Your ELSER model has deployed but not started.' } + )} +

+
+
+ {isDismissable && ( + + + + )} +
+
+ + +

+ {i18n.translate( + 'xpack.enterpriseSearch.content.index.pipelines.textExpansionCallOut.deployedBody', + { + defaultMessage: + 'You may start the model in a single-threaded configuration for testing, or tune the performance for a production environment.', + } + )} +

+
+
+ + + + + + + startTextExpansionModel(undefined)} + > + {i18n.translate( + 'xpack.enterpriseSearch.content.indices.pipelines.textExpansionCallOut.startModelButton.label', + { + defaultMessage: 'Start single-threaded', + } + )} + + + + + + + +
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_deployment_in_progress.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_deployment_in_progress.test.tsx new file mode 100644 index 0000000000000..f147778539f55 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_deployment_in_progress.test.tsx @@ -0,0 +1,39 @@ +/* + * 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 React from 'react'; + +import { shallow } from 'enzyme'; + +import { ModelDeploymentInProgress } from './model_deployment_in_progress'; +import { TextExpansionDismissButton } from './text_expansion_callout'; + +const DEFAULT_VALUES = { + startTextExpansionModelError: undefined, + isCreateButtonDisabled: false, + isModelDownloadInProgress: false, + isModelDownloaded: false, + isModelStarted: false, + isStartButtonDisabled: false, +}; + +describe('ModelDeploymentInProgress', () => { + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(DEFAULT_VALUES); + }); + it('renders dismiss button if it is set to dismissable', () => { + const wrapper = shallow( {}} isDismissable />); + expect(wrapper.find(TextExpansionDismissButton).length).toBe(1); + }); + it('does not render dismiss button if it is set to non-dismissable', () => { + const wrapper = shallow( {}} isDismissable={false} />); + expect(wrapper.find(TextExpansionDismissButton).length).toBe(0); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_deployment_in_progress.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_deployment_in_progress.tsx new file mode 100644 index 0000000000000..f9b9439833255 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_deployment_in_progress.tsx @@ -0,0 +1,58 @@ +/* + * 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 { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiText, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { TextExpansionCallOutState, TextExpansionDismissButton } from './text_expansion_callout'; + +export const ModelDeploymentInProgress = ({ + dismiss, + isDismissable, +}: Pick) => ( + + + + + + + + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.content.index.pipelines.textExpansionCallOut.deployingTitle', + { defaultMessage: 'Your ELSER model is deploying.' } + )} +

+
+
+ {isDismissable && ( + + + + )} +
+
+ + +

+ {i18n.translate( + 'xpack.enterpriseSearch.content.index.pipelines.textExpansionCallOut.deployingBody', + { + defaultMessage: + 'You can continue creating your pipeline with other uploaded models in the meantime.', + } + )} +

+
+
+
+
+); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_started.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_started.test.tsx new file mode 100644 index 0000000000000..c98ca42a41121 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_started.test.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 { setMockValues } from '../../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiText } from '@elastic/eui'; + +import { ModelStarted } from './model_started'; +import { TextExpansionDismissButton, FineTuneModelsButton } from './text_expansion_callout'; + +const DEFAULT_VALUES = { + startTextExpansionModelError: undefined, + isCreateButtonDisabled: false, + isModelDownloadInProgress: false, + isModelDownloaded: false, + isModelStarted: false, + isStartButtonDisabled: false, +}; + +describe('ModelStarted', () => { + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(DEFAULT_VALUES); + }); + it('renders dismiss button if it is set to dismissable', () => { + const wrapper = shallow( + {}} isCompact={false} isDismissable isSingleThreaded /> + ); + expect(wrapper.find(TextExpansionDismissButton).length).toBe(1); + }); + it('does not render dismiss button if it is set to non-dismissable', () => { + const wrapper = shallow( + {}} isCompact={false} isDismissable={false} isSingleThreaded /> + ); + expect(wrapper.find(TextExpansionDismissButton).length).toBe(0); + }); + it('renders fine-tune button if the model is running single-threaded', () => { + const wrapper = shallow( + {}} isCompact={false} isDismissable isSingleThreaded /> + ); + expect(wrapper.find(FineTuneModelsButton).length).toBe(1); + }); + it('does not render description if it is set to compact', () => { + const wrapper = shallow( + {}} isCompact isDismissable isSingleThreaded /> + ); + expect(wrapper.find(EuiText).length).toBe(1); // Title only + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_started.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_started.tsx new file mode 100644 index 0000000000000..3c400854a5df0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/model_started.tsx @@ -0,0 +1,135 @@ +/* + * 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 { + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiText, + EuiButtonEmpty, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { KibanaLogic } from '../../../../../shared/kibana'; + +import { + TextExpansionCallOutState, + TextExpansionDismissButton, + FineTuneModelsButton, +} from './text_expansion_callout'; +import { TRAINED_MODELS_PATH } from './utils'; + +export const ModelStarted = ({ + dismiss, + isCompact, + isDismissable, + isSingleThreaded, +}: Pick< + TextExpansionCallOutState, + 'dismiss' | 'isCompact' | 'isDismissable' | 'isSingleThreaded' +>) => ( + + + + + + + + + +

+ {isSingleThreaded + ? isCompact + ? i18n.translate( + 'xpack.enterpriseSearch.content.index.pipelines.textExpansionCallOut.startedSingleThreadedTitleCompact', + { defaultMessage: 'Your ELSER model is running single-threaded.' } + ) + : i18n.translate( + 'xpack.enterpriseSearch.content.index.pipelines.textExpansionCallOut.startedSingleThreadedTitle', + { defaultMessage: 'Your ELSER model has started single-threaded.' } + ) + : isCompact + ? i18n.translate( + 'xpack.enterpriseSearch.content.index.pipelines.textExpansionCallOut.startedTitleCompact', + { defaultMessage: 'Your ELSER model is running.' } + ) + : i18n.translate( + 'xpack.enterpriseSearch.content.index.pipelines.textExpansionCallOut.startedTitle', + { defaultMessage: 'Your ELSER model has started.' } + )} +

+
+
+ {isDismissable && ( + + + + )} +
+
+ {!isCompact && ( + <> + + +

+ {isSingleThreaded + ? i18n.translate( + 'xpack.enterpriseSearch.content.index.pipelines.textExpansionCallOut.startedSingleThreadedBody', + { + defaultMessage: + 'This single-threaded configuration is great for testing your custom inference pipelines, however performance should be fine-tuned for production.', + } + ) + : i18n.translate( + 'xpack.enterpriseSearch.content.index.pipelines.textExpansionCallOut.startedBody', + { + defaultMessage: + 'Enjoy the power of ELSER in your custom Inference pipeline.', + } + )} +

+
+
+ + + + {isSingleThreaded ? ( + + ) : ( + + KibanaLogic.values.navigateToUrl(TRAINED_MODELS_PATH, { + shouldNotCreateHref: true, + }) + } + > + {i18n.translate( + 'xpack.enterpriseSearch.content.indices.pipelines.textExpansionCallOut.viewModelsButton', + { + defaultMessage: 'View details', + } + )} + + )} + + + + + )} +
+
+); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/text_expansion_callout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/text_expansion_callout.test.tsx index 2b7b28732fc91..a1ee2410128c1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/text_expansion_callout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/text_expansion_callout.test.tsx @@ -11,20 +11,13 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiButton, EuiText } from '@elastic/eui'; - import { HttpError } from '../../../../../../../common/types/api'; -import { - TextExpansionCallOut, - DeployModel, - ModelDeploymentInProgress, - ModelDeployed, - TextExpansionDismissButton, - ModelStarted, - FineTuneModelsButton, -} from './text_expansion_callout'; - +import { DeployModel } from './deploy_model'; +import { ModelDeployed } from './model_deployed'; +import { ModelDeploymentInProgress } from './model_deployment_in_progress'; +import { ModelStarted } from './model_started'; +import { TextExpansionCallOut } from './text_expansion_callout'; import { TextExpansionErrors } from './text_expansion_errors'; jest.mock('./text_expansion_callout_data', () => ({ @@ -97,146 +90,4 @@ describe('TextExpansionCallOut', () => { const wrapper = shallow(); expect(wrapper.find(ModelStarted).length).toBe(1); }); - - describe('DeployModel', () => { - it('renders deploy button', () => { - const wrapper = shallow( - {}} - ingestionMethod="crawler" - isCreateButtonDisabled={false} - isDismissable={false} - /> - ); - expect(wrapper.find(EuiButton).length).toBe(1); - const button = wrapper.find(EuiButton); - expect(button.prop('disabled')).toBe(false); - }); - it('renders disabled deploy button if it is set to disabled', () => { - const wrapper = shallow( - {}} - ingestionMethod="crawler" - isCreateButtonDisabled - isDismissable={false} - /> - ); - expect(wrapper.find(EuiButton).length).toBe(1); - const button = wrapper.find(EuiButton); - expect(button.prop('disabled')).toBe(true); - }); - it('renders dismiss button if it is set to dismissable', () => { - const wrapper = shallow( - {}} - ingestionMethod="crawler" - isCreateButtonDisabled={false} - isDismissable - /> - ); - expect(wrapper.find(TextExpansionDismissButton).length).toBe(1); - }); - it('does not render dismiss button if it is set to non-dismissable', () => { - const wrapper = shallow( - {}} - ingestionMethod="crawler" - isCreateButtonDisabled={false} - isDismissable={false} - /> - ); - expect(wrapper.find(TextExpansionDismissButton).length).toBe(0); - }); - }); - - describe('ModelDeploymentInProgress', () => { - it('renders dismiss button if it is set to dismissable', () => { - const wrapper = shallow( {}} isDismissable />); - expect(wrapper.find(TextExpansionDismissButton).length).toBe(1); - }); - it('does not render dismiss button if it is set to non-dismissable', () => { - const wrapper = shallow( - {}} isDismissable={false} /> - ); - expect(wrapper.find(TextExpansionDismissButton).length).toBe(0); - }); - }); - - describe('ModelDeployed', () => { - it('renders start button', () => { - const wrapper = shallow( - {}} - ingestionMethod="crawler" - isDismissable={false} - isStartButtonDisabled={false} - /> - ); - expect(wrapper.find(EuiButton).length).toBe(1); - const button = wrapper.find(EuiButton); - expect(button.prop('disabled')).toBe(false); - }); - it('renders disabled start button if it is set to disabled', () => { - const wrapper = shallow( - {}} - ingestionMethod="crawler" - isDismissable={false} - isStartButtonDisabled - /> - ); - expect(wrapper.find(EuiButton).length).toBe(1); - const button = wrapper.find(EuiButton); - expect(button.prop('disabled')).toBe(true); - }); - it('renders dismiss button if it is set to dismissable', () => { - const wrapper = shallow( - {}} - ingestionMethod="crawler" - isDismissable - isStartButtonDisabled={false} - /> - ); - expect(wrapper.find(TextExpansionDismissButton).length).toBe(1); - }); - it('does not render dismiss button if it is set to non-dismissable', () => { - const wrapper = shallow( - {}} - ingestionMethod="crawler" - isDismissable={false} - isStartButtonDisabled={false} - /> - ); - expect(wrapper.find(TextExpansionDismissButton).length).toBe(0); - }); - }); - - describe('ModelStarted', () => { - it('renders dismiss button if it is set to dismissable', () => { - const wrapper = shallow( - {}} isCompact={false} isDismissable isSingleThreaded /> - ); - expect(wrapper.find(TextExpansionDismissButton).length).toBe(1); - }); - it('does not render dismiss button if it is set to non-dismissable', () => { - const wrapper = shallow( - {}} isCompact={false} isDismissable={false} isSingleThreaded /> - ); - expect(wrapper.find(TextExpansionDismissButton).length).toBe(0); - }); - it('renders fine-tune button if the model is running single-threaded', () => { - const wrapper = shallow( - {}} isCompact={false} isDismissable isSingleThreaded /> - ); - expect(wrapper.find(FineTuneModelsButton).length).toBe(1); - }); - it('does not render description if it is set to compact', () => { - const wrapper = shallow( - {}} isCompact isDismissable isSingleThreaded /> - ); - expect(wrapper.find(EuiText).length).toBe(1); // Title only - }); - }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/text_expansion_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/text_expansion_callout.tsx index 6743d79d820b9..7ded5c3e9035d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/text_expansion_callout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/text_expansion_callout.tsx @@ -7,32 +7,22 @@ import React from 'react'; -import { useActions, useValues } from 'kea'; +import { useValues } from 'kea'; -import { - EuiBadge, - EuiButton, - EuiButtonEmpty, - EuiButtonIcon, - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiLink, - EuiSpacer, - EuiText, -} from '@elastic/eui'; +import { EuiButtonEmpty, EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage, FormattedHTMLMessage } from '@kbn/i18n-react'; -import { docLinks } from '../../../../../shared/doc_links'; import { KibanaLogic } from '../../../../../shared/kibana'; - import { IndexViewLogic } from '../../index_view_logic'; +import { DeployModel } from './deploy_model'; +import { ModelDeployed } from './model_deployed'; +import { ModelDeploymentInProgress } from './model_deployment_in_progress'; +import { ModelStarted } from './model_started'; import { useTextExpansionCallOutData } from './text_expansion_callout_data'; import { getTextExpansionError, TextExpansionCalloutLogic } from './text_expansion_callout_logic'; import { TextExpansionErrors } from './text_expansion_errors'; +import { TRAINED_MODELS_PATH } from './utils'; export interface TextExpansionCallOutState { dismiss: () => void; @@ -50,8 +40,6 @@ export interface TextExpansionCallOutProps { isDismissable?: boolean; } -const TRAINED_MODELS_PATH = '/app/ml/trained_models'; - export const TextExpansionDismissButton = ({ dismiss, }: Pick) => { @@ -86,336 +74,6 @@ export const FineTuneModelsButton: React.FC = () => ( ); -export const DeployModel = ({ - dismiss, - ingestionMethod, - isCreateButtonDisabled, - isDismissable, -}: Pick< - TextExpansionCallOutState, - 'dismiss' | 'ingestionMethod' | 'isCreateButtonDisabled' | 'isDismissable' ->) => { - const { createTextExpansionModel } = useActions(TextExpansionCalloutLogic); - - return ( - - - - - - - - - - - -

- {i18n.translate( - 'xpack.enterpriseSearch.content.index.pipelines.textExpansionCallOut.title', - { defaultMessage: 'Improve your results with ELSER' } - )} -

-
-
- {isDismissable && ( - - - - )} -
-
- - - - - - - - - - - createTextExpansionModel(undefined)} - > - {i18n.translate( - 'xpack.enterpriseSearch.content.indices.pipelines.textExpansionCallOut.deployButton.label', - { - defaultMessage: 'Deploy', - } - )} - - - - - - - - - - - -
-
- ); -}; - -export const ModelDeploymentInProgress = ({ - dismiss, - isDismissable, -}: Pick) => ( - - - - - - - - - -

- {i18n.translate( - 'xpack.enterpriseSearch.content.index.pipelines.textExpansionCallOut.deployingTitle', - { defaultMessage: 'Your ELSER model is deploying.' } - )} -

-
-
- {isDismissable && ( - - - - )} -
-
- - -

- {i18n.translate( - 'xpack.enterpriseSearch.content.index.pipelines.textExpansionCallOut.deployingBody', - { - defaultMessage: - 'You can continue creating your pipeline with other uploaded models in the meantime.', - } - )} -

-
-
-
-
-); - -export const ModelDeployed = ({ - dismiss, - ingestionMethod, - isDismissable, - isStartButtonDisabled, -}: Pick< - TextExpansionCallOutState, - 'dismiss' | 'ingestionMethod' | 'isDismissable' | 'isStartButtonDisabled' ->) => { - const { startTextExpansionModel } = useActions(TextExpansionCalloutLogic); - - return ( - - - - - - - - - -

- {i18n.translate( - 'xpack.enterpriseSearch.content.index.pipelines.textExpansionCallOut.deployedTitle', - { defaultMessage: 'Your ELSER model has deployed but not started.' } - )} -

-
-
- {isDismissable && ( - - - - )} -
-
- - -

- {i18n.translate( - 'xpack.enterpriseSearch.content.index.pipelines.textExpansionCallOut.deployedBody', - { - defaultMessage: - 'You may start the model in a single-threaded configuration for testing, or tune the performance for a production environment.', - } - )} -

-
-
- - - - - - - startTextExpansionModel(undefined)} - > - {i18n.translate( - 'xpack.enterpriseSearch.content.indices.pipelines.textExpansionCallOut.startModelButton.label', - { - defaultMessage: 'Start single-threaded', - } - )} - - - - - - - -
-
- ); -}; - -export const ModelStarted = ({ - dismiss, - isCompact, - isDismissable, - isSingleThreaded, -}: Pick< - TextExpansionCallOutState, - 'dismiss' | 'isCompact' | 'isDismissable' | 'isSingleThreaded' ->) => ( - - - - - - - - - -

- {isSingleThreaded - ? isCompact - ? i18n.translate( - 'xpack.enterpriseSearch.content.index.pipelines.textExpansionCallOut.startedSingleThreadedTitleCompact', - { defaultMessage: 'Your ELSER model is running single-threaded.' } - ) - : i18n.translate( - 'xpack.enterpriseSearch.content.index.pipelines.textExpansionCallOut.startedSingleThreadedTitle', - { defaultMessage: 'Your ELSER model has started single-threaded.' } - ) - : isCompact - ? i18n.translate( - 'xpack.enterpriseSearch.content.index.pipelines.textExpansionCallOut.startedTitleCompact', - { defaultMessage: 'Your ELSER model is running.' } - ) - : i18n.translate( - 'xpack.enterpriseSearch.content.index.pipelines.textExpansionCallOut.startedTitle', - { defaultMessage: 'Your ELSER model has started.' } - )} -

-
-
- {isDismissable && ( - - - - )} -
-
- {!isCompact && ( - <> - - -

- {isSingleThreaded - ? i18n.translate( - 'xpack.enterpriseSearch.content.index.pipelines.textExpansionCallOut.startedSingleThreadedBody', - { - defaultMessage: - 'This single-threaded configuration is great for testing your custom inference pipelines, however performance should be fine-tuned for production.', - } - ) - : i18n.translate( - 'xpack.enterpriseSearch.content.index.pipelines.textExpansionCallOut.startedBody', - { - defaultMessage: - 'Enjoy the power of ELSER in your custom Inference pipeline.', - } - )} -

-
-
- - - - {isSingleThreaded ? ( - - ) : ( - - KibanaLogic.values.navigateToUrl(TRAINED_MODELS_PATH, { - shouldNotCreateHref: true, - }) - } - > - {i18n.translate( - 'xpack.enterpriseSearch.content.indices.pipelines.textExpansionCallOut.viewModelsButton', - { - defaultMessage: 'View details', - } - )} - - )} - - - - - )} -
-
-); - export const TextExpansionCallOut: React.FC = (props) => { const { dismiss, isCompact, isDismissable, show } = useTextExpansionCallOutData(props); const { ingestionMethod } = useValues(IndexViewLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/utils.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/utils.ts index e70468d684279..d05b1453547bd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/utils.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/pipelines/ml_inference/utils.ts @@ -12,6 +12,8 @@ import { FetchPipelineResponse } from '../../../../api/pipelines/fetch_pipeline' import { AddInferencePipelineFormErrors, InferencePipelineConfiguration } from './types'; const VALID_PIPELINE_NAME_REGEX = /^[\w\-]+$/; +export const TRAINED_MODELS_PATH = '/app/ml/trained_models'; + export const isValidPipelineName = (input: string): boolean => { return input.length > 0 && VALID_PIPELINE_NAME_REGEX.test(input); }; diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.mock.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.mock.ts index 6659f0b19ebeb..2a5582347db74 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.mock.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.mock.ts @@ -11,8 +11,6 @@ const createClusterClientMock = () => { const mock: jest.Mocked = { indexDocument: jest.fn(), indexDocuments: jest.fn(), - doesIlmPolicyExist: jest.fn(), - createIlmPolicy: jest.fn(), doesIndexTemplateExist: jest.fn(), createIndexTemplate: jest.fn(), doesDataStreamExist: jest.fn(), diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts index 151a65573125c..6f36af2be1f78 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.test.ts @@ -165,56 +165,6 @@ describe('buffering documents', () => { }); }); -describe('doesIlmPolicyExist', () => { - // ElasticsearchError can be a bit random in shape, we need an any here - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const notFoundError = new Error('Not found') as any; - notFoundError.statusCode = 404; - - test('should call cluster with proper arguments', async () => { - await clusterClientAdapter.doesIlmPolicyExist('foo'); - expect(clusterClient.transport.request).toHaveBeenCalledWith({ - method: 'GET', - path: '/_ilm/policy/foo', - }); - }); - - test('should return false when 404 error is returned by Elasticsearch', async () => { - clusterClient.transport.request.mockRejectedValue(notFoundError); - await expect(clusterClientAdapter.doesIlmPolicyExist('foo')).resolves.toEqual(false); - }); - - test('should throw error when error is not 404', async () => { - clusterClient.transport.request.mockRejectedValue(new Error('Fail')); - await expect( - clusterClientAdapter.doesIlmPolicyExist('foo') - ).rejects.toThrowErrorMatchingInlineSnapshot(`"error checking existance of ilm policy: Fail"`); - }); - - test('should return true when no error is thrown', async () => { - await expect(clusterClientAdapter.doesIlmPolicyExist('foo')).resolves.toEqual(true); - }); -}); - -describe('createIlmPolicy', () => { - test('should call cluster client with given policy', async () => { - clusterClient.transport.request.mockResolvedValue({ success: true }); - await clusterClientAdapter.createIlmPolicy('foo', { args: true }); - expect(clusterClient.transport.request).toHaveBeenCalledWith({ - method: 'PUT', - path: '/_ilm/policy/foo', - body: { args: true }, - }); - }); - - test('should throw error when call cluster client throws', async () => { - clusterClient.transport.request.mockRejectedValue(new Error('Fail')); - await expect( - clusterClientAdapter.createIlmPolicy('foo', { args: true }) - ).rejects.toThrowErrorMatchingInlineSnapshot(`"error creating ilm policy: Fail"`); - }); -}); - describe('doesIndexTemplateExist', () => { test('should call cluster with proper arguments', async () => { await clusterClientAdapter.doesIndexTemplateExist('foo'); diff --git a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts index 8807e34cfedf3..27a86b839d6c9 100644 --- a/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts +++ b/x-pack/plugins/event_log/server/es/cluster_client_adapter.ts @@ -178,36 +178,6 @@ export class ClusterClientAdapter { - const request = { - method: 'GET', - path: `/_ilm/policy/${policyName}`, - }; - try { - const esClient = await this.elasticsearchClientPromise; - await esClient.transport.request(request); - } catch (err) { - if (err.statusCode === 404) return false; - throw new Error(`error checking existance of ilm policy: ${err.message}`); - } - return true; - } - - public async createIlmPolicy(policyName: string, policy: Record): Promise { - this.logger.info(`Installing ILM policy ${policyName}`); - const request = { - method: 'PUT', - path: `/_ilm/policy/${policyName}`, - body: policy, - }; - try { - const esClient = await this.elasticsearchClientPromise; - await esClient.transport.request(request); - } catch (err) { - throw new Error(`error creating ilm policy: ${err.message}`); - } - } - public async doesIndexTemplateExist(name: string): Promise { try { const esClient = await this.elasticsearchClientPromise; diff --git a/x-pack/plugins/event_log/server/es/context.test.ts b/x-pack/plugins/event_log/server/es/context.test.ts index 67ea2a95151f2..681b927478d81 100644 --- a/x-pack/plugins/event_log/server/es/context.test.ts +++ b/x-pack/plugins/event_log/server/es/context.test.ts @@ -57,13 +57,12 @@ describe('createEsContext', () => { expect(esNames).toStrictEqual({ base: 'test-index', dataStream: 'test-index-event-log-1.2.3', - ilmPolicy: 'test-index-event-log-policy', indexPattern: 'test-index-event-log-*', indexTemplate: 'test-index-event-log-1.2.3-template', }); }); - test('should return exist false for esAdapter ilm policy, index template and data stream before initialize', async () => { + test('should return exist false for esAdapter index template and data stream before initialize', async () => { const context = createEsContext({ logger, indexNameRoot: 'test1', @@ -84,7 +83,7 @@ describe('createEsContext', () => { expect(doesIndexTemplateExist).toBeFalsy(); }); - test('should return exist true for esAdapter ilm policy, index template and data stream after initialize', async () => { + test('should return exist true for esAdapter index template and data stream after initialize', async () => { const context = createEsContext({ logger, indexNameRoot: 'test2', @@ -94,11 +93,6 @@ describe('createEsContext', () => { elasticsearchClient.indices.existsTemplate.mockResponse(true); context.initialize(); - const doesIlmPolicyExist = await context.esAdapter.doesIlmPolicyExist( - context.esNames.ilmPolicy - ); - expect(doesIlmPolicyExist).toBeTruthy(); - elasticsearchClient.indices.getDataStream.mockResolvedValue(GetDataStreamsResponse); const doesDataStreamExist = await context.esAdapter.doesDataStreamExist( context.esNames.dataStream diff --git a/x-pack/plugins/event_log/server/es/documents.test.ts b/x-pack/plugins/event_log/server/es/documents.test.ts index 814596f751c61..71b75ee3ca3dc 100644 --- a/x-pack/plugins/event_log/server/es/documents.test.ts +++ b/x-pack/plugins/event_log/server/es/documents.test.ts @@ -5,19 +5,9 @@ * 2.0. */ -import { getIndexTemplate, getIlmPolicy } from './documents'; +import { getIndexTemplate } from './documents'; import { getEsNames } from './names'; -describe('getIlmPolicy()', () => { - test('returns the basic structure of an ilm policy', () => { - expect(getIlmPolicy()).toMatchObject({ - policy: { - phases: {}, - }, - }); - }); -}); - describe('getIndexTemplate()', () => { const kibanaVersion = '1.2.3'; const esNames = getEsNames('XYZ', kibanaVersion); @@ -27,7 +17,6 @@ describe('getIndexTemplate()', () => { expect(indexTemplate.index_patterns).toEqual([esNames.dataStream]); expect(indexTemplate.template.settings.number_of_shards).toBeGreaterThanOrEqual(0); expect(indexTemplate.template.settings.auto_expand_replicas).toBe('0-1'); - expect(indexTemplate.template.settings['index.lifecycle.name']).toBe(esNames.ilmPolicy); expect(indexTemplate.template.mappings).toMatchObject({}); }); }); diff --git a/x-pack/plugins/event_log/server/es/documents.ts b/x-pack/plugins/event_log/server/es/documents.ts index deaa8349971f9..0f654f80ad55b 100644 --- a/x-pack/plugins/event_log/server/es/documents.ts +++ b/x-pack/plugins/event_log/server/es/documents.ts @@ -25,7 +25,9 @@ export function getIndexTemplate(esNames: EsNames) { hidden: true, number_of_shards: 1, auto_expand_replicas: '0-1', - 'index.lifecycle.name': esNames.ilmPolicy, + }, + lifecycle: { + data_retention: '90d', }, mappings, }, @@ -33,33 +35,3 @@ export function getIndexTemplate(esNames: EsNames) { return indexTemplateBody; } - -// returns the body of an ilm policy used in an ES PUT _ilm/policy call -export function getIlmPolicy() { - return { - policy: { - _meta: { - description: - 'ilm policy the Kibana event log, created initially by Kibana, but updated by the user, not Kibana', - managed: false, - }, - phases: { - hot: { - actions: { - rollover: { - max_size: '50GB', - max_age: '30d', - // max_docs: 1, // you know, for testing - }, - }, - }, - delete: { - min_age: '90d', - actions: { - delete: {}, - }, - }, - }, - }, - }; -} diff --git a/x-pack/plugins/event_log/server/es/init.test.ts b/x-pack/plugins/event_log/server/es/init.test.ts index 30e220313b26b..7c81ae80b8823 100644 --- a/x-pack/plugins/event_log/server/es/init.test.ts +++ b/x-pack/plugins/event_log/server/es/init.test.ts @@ -83,7 +83,6 @@ describe('initializeEs', () => { `error getting existing index templates - Fail` ); expect(esContext.esAdapter.setLegacyIndexTemplateToHidden).not.toHaveBeenCalled(); - expect(esContext.esAdapter.doesIlmPolicyExist).toHaveBeenCalled(); }); test(`should continue initialization if updating existing index templates throws an error`, async () => { @@ -124,7 +123,6 @@ describe('initializeEs', () => { expect(esContext.logger.error).toHaveBeenCalledWith( `error setting existing \"foo-bar-template\" index template to hidden - Fail` ); - expect(esContext.esAdapter.doesIlmPolicyExist).toHaveBeenCalled(); }); test(`should update existing index settings if any exist and are not hidden`, async () => { @@ -207,7 +205,6 @@ describe('initializeEs', () => { expect(esContext.esAdapter.getExistingIndices).toHaveBeenCalled(); expect(esContext.logger.error).toHaveBeenCalledWith(`error getting existing indices - Fail`); expect(esContext.esAdapter.setIndexToHidden).not.toHaveBeenCalled(); - expect(esContext.esAdapter.doesIlmPolicyExist).toHaveBeenCalled(); }); test(`should continue initialization if updating existing index settings throws an error`, async () => { @@ -251,7 +248,6 @@ describe('initializeEs', () => { expect(esContext.logger.error).toHaveBeenCalledWith( `error setting existing \"foo-bar-000001\" index to hidden - Fail` ); - expect(esContext.esAdapter.doesIlmPolicyExist).toHaveBeenCalled(); }); test(`should update existing index aliases if any exist and are not hidden`, async () => { @@ -300,7 +296,6 @@ describe('initializeEs', () => { `error getting existing index aliases - Fail` ); expect(esContext.esAdapter.setIndexAliasToHidden).not.toHaveBeenCalled(); - expect(esContext.esAdapter.doesIlmPolicyExist).toHaveBeenCalled(); }); test(`should continue initialization if updating existing index aliases throws an error`, async () => { @@ -336,23 +331,6 @@ describe('initializeEs', () => { expect(esContext.logger.error).toHaveBeenCalledWith( `error setting existing \"foo-bar\" index aliases - Fail` ); - expect(esContext.esAdapter.doesIlmPolicyExist).toHaveBeenCalled(); - }); - - test(`should create ILM policy if it doesn't exist`, async () => { - esContext.esAdapter.doesIlmPolicyExist.mockResolvedValue(false); - - await initializeEs(esContext); - expect(esContext.esAdapter.doesIlmPolicyExist).toHaveBeenCalled(); - expect(esContext.esAdapter.createIlmPolicy).toHaveBeenCalled(); - }); - - test(`shouldn't create ILM policy if it exists`, async () => { - esContext.esAdapter.doesIlmPolicyExist.mockResolvedValue(true); - - await initializeEs(esContext); - expect(esContext.esAdapter.doesIlmPolicyExist).toHaveBeenCalled(); - expect(esContext.esAdapter.createIlmPolicy).not.toHaveBeenCalled(); }); test(`should create index template if it doesn't exist`, async () => { @@ -463,30 +441,10 @@ describe('retries', () => { esContext.esAdapter.getExistingLegacyIndexTemplates.mockResolvedValue({}); esContext.esAdapter.getExistingIndices.mockResolvedValue({}); esContext.esAdapter.getExistingIndexAliases.mockResolvedValue({}); - esContext.esAdapter.doesIlmPolicyExist.mockResolvedValue(true); esContext.esAdapter.doesIndexTemplateExist.mockResolvedValue(true); esContext.esAdapter.doesDataStreamExist.mockResolvedValue(true); }); - test('createIlmPolicyIfNotExists with 1 retry', async () => { - esContext.esAdapter.doesIlmPolicyExist.mockRejectedValueOnce(new Error('retry 1')); - - const timeStart = performance.now(); - await initializeEs(esContext); - const timeElapsed = Math.ceil(performance.now() - timeStart); - - expect(timeElapsed).toBeGreaterThanOrEqual(MOCK_RETRY_DELAY); - - expect(esContext.esAdapter.getExistingLegacyIndexTemplates).toHaveBeenCalledTimes(1); - expect(esContext.esAdapter.doesIlmPolicyExist).toHaveBeenCalledTimes(2); - expect(esContext.esAdapter.doesIndexTemplateExist).toHaveBeenCalledTimes(1); - expect(esContext.esAdapter.doesDataStreamExist).toHaveBeenCalledTimes(1); - - const prefix = `eventLog initialization operation failed and will be retried: createIlmPolicyIfNotExists`; - expect(esContext.logger.warn).toHaveBeenCalledTimes(1); - expect(esContext.logger.warn).toHaveBeenCalledWith(`${prefix}; 4 more times; error: retry 1`); - }); - test('createIndexTemplateIfNotExists with 2 retries', async () => { esContext.esAdapter.doesIndexTemplateExist.mockRejectedValueOnce(new Error('retry 2a')); esContext.esAdapter.doesIndexTemplateExist.mockRejectedValueOnce(new Error('retry 2b')); @@ -498,7 +456,6 @@ describe('retries', () => { expect(timeElapsed).toBeGreaterThanOrEqual(MOCK_RETRY_DELAY * (1 + 2)); expect(esContext.esAdapter.getExistingLegacyIndexTemplates).toHaveBeenCalledTimes(1); - expect(esContext.esAdapter.doesIlmPolicyExist).toHaveBeenCalledTimes(1); expect(esContext.esAdapter.doesIndexTemplateExist).toHaveBeenCalledTimes(3); expect(esContext.esAdapter.doesDataStreamExist).toHaveBeenCalledTimes(1); @@ -524,7 +481,6 @@ describe('retries', () => { expect(timeElapsed).toBeGreaterThanOrEqual(MOCK_RETRY_DELAY * (1 + 2 + 4 + 8)); expect(esContext.esAdapter.getExistingLegacyIndexTemplates).toHaveBeenCalledTimes(1); - expect(esContext.esAdapter.doesIlmPolicyExist).toHaveBeenCalledTimes(1); expect(esContext.esAdapter.doesIndexTemplateExist).toHaveBeenCalledTimes(1); expect(esContext.esAdapter.doesDataStreamExist).toHaveBeenCalledTimes(5); expect(esContext.esAdapter.createDataStream).toHaveBeenCalledTimes(0); diff --git a/x-pack/plugins/event_log/server/es/init.ts b/x-pack/plugins/event_log/server/es/init.ts index 6eb4d5736a4a1..cf737cbf035c6 100644 --- a/x-pack/plugins/event_log/server/es/init.ts +++ b/x-pack/plugins/event_log/server/es/init.ts @@ -9,7 +9,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { asyncForEach } from '@kbn/std'; import { groupBy } from 'lodash'; import pRetry, { FailedAttemptError } from 'p-retry'; -import { getIlmPolicy, getIndexTemplate } from './documents'; +import { getIndexTemplate } from './documents'; import { EsContext } from './context'; const MAX_RETRY_DELAY = 30000; @@ -33,7 +33,6 @@ async function initializeEsResources(esContext: EsContext) { // today, setExistingAssetsToHidden() never throws, but just in case ... await retry(steps.setExistingAssetsToHidden); - await retry(steps.createIlmPolicyIfNotExists); await retry(steps.createIndexTemplateIfNotExists); await retry(steps.createDataStreamIfNotExists); @@ -202,18 +201,6 @@ class EsInitializationSteps { await this.setExistingIndexAliasesToHidden(); } - async createIlmPolicyIfNotExists(): Promise { - const exists = await this.esContext.esAdapter.doesIlmPolicyExist( - this.esContext.esNames.ilmPolicy - ); - if (!exists) { - await this.esContext.esAdapter.createIlmPolicy( - this.esContext.esNames.ilmPolicy, - getIlmPolicy() - ); - } - } - async createIndexTemplateIfNotExists(): Promise { const exists = await this.esContext.esAdapter.doesIndexTemplateExist( this.esContext.esNames.indexTemplate diff --git a/x-pack/plugins/event_log/server/es/names.mock.ts b/x-pack/plugins/event_log/server/es/names.mock.ts index 138d99aa706ea..837abe9dd413b 100644 --- a/x-pack/plugins/event_log/server/es/names.mock.ts +++ b/x-pack/plugins/event_log/server/es/names.mock.ts @@ -11,7 +11,6 @@ const createNamesMock = () => { const mock: jest.Mocked = { base: '.kibana', dataStream: '.kibana-event-log-8.0.0', - ilmPolicy: 'kibana-event-log-policy', indexPattern: '.kibana-event-log-*', indexTemplate: '.kibana-event-log-8.0.0-template', }; diff --git a/x-pack/plugins/event_log/server/es/names.test.ts b/x-pack/plugins/event_log/server/es/names.test.ts index 0a05d560b9c94..63d1ad9d398a7 100644 --- a/x-pack/plugins/event_log/server/es/names.test.ts +++ b/x-pack/plugins/event_log/server/es/names.test.ts @@ -18,16 +18,7 @@ describe('getEsNames()', () => { const esNames = getEsNames(base, kibanaVersion); expect(esNames.base).toEqual(base); expect(esNames.dataStream).toEqual(`${base}-event-log-${kibanaVersion}`); - expect(esNames.ilmPolicy).toEqual(`${base}-event-log-policy`); expect(esNames.indexPattern).toEqual(`${base}-event-log-*`); expect(esNames.indexTemplate).toEqual(`${base}-event-log-${kibanaVersion}-template`); }); - - test('ilm policy name does not contain dot prefix', () => { - const base = '.XYZ'; - const kibanaVersion = '1.2.3'; - - const esNames = getEsNames(base, kibanaVersion); - expect(esNames.ilmPolicy).toEqual('XYZ-event-log-policy'); - }); }); diff --git a/x-pack/plugins/event_log/server/es/names.ts b/x-pack/plugins/event_log/server/es/names.ts index d807e53c6abbb..0e48ca911b95a 100644 --- a/x-pack/plugins/event_log/server/es/names.ts +++ b/x-pack/plugins/event_log/server/es/names.ts @@ -10,7 +10,6 @@ const EVENT_LOG_NAME_SUFFIX = `-event-log`; export interface EsNames { base: string; dataStream: string; - ilmPolicy: string; indexPattern: string; indexTemplate: string; } @@ -19,13 +18,9 @@ export function getEsNames(baseName: string, kibanaVersion: string): EsNames { const EVENT_LOG_VERSION_SUFFIX = `-${kibanaVersion.toLocaleLowerCase()}`; const eventLogName = `${baseName}${EVENT_LOG_NAME_SUFFIX}`; const eventLogNameWithVersion = `${eventLogName}${EVENT_LOG_VERSION_SUFFIX}`; - const eventLogPolicyName = `${ - baseName.startsWith('.') ? baseName.substring(1) : baseName - }${EVENT_LOG_NAME_SUFFIX}-policy`; return { base: baseName, dataStream: eventLogNameWithVersion, - ilmPolicy: `${eventLogPolicyName}`, indexPattern: `${eventLogName}-*`, indexTemplate: `${eventLogNameWithVersion}-template`, }; diff --git a/x-pack/plugins/fleet/common/constants/secrets.ts b/x-pack/plugins/fleet/common/constants/secrets.ts index 4c42b1d68013e..06d370ff81323 100644 --- a/x-pack/plugins/fleet/common/constants/secrets.ts +++ b/x-pack/plugins/fleet/common/constants/secrets.ts @@ -5,4 +5,4 @@ * 2.0. */ -export const SECRETS_INDEX = '.fleet-secrets'; +export const SECRETS_ENDPOINT_PATH = '/_fleet/secret'; diff --git a/x-pack/plugins/fleet/common/types/models/secret.ts b/x-pack/plugins/fleet/common/types/models/secret.ts index dea38da64cce8..cf95d88b82e2b 100644 --- a/x-pack/plugins/fleet/common/types/models/secret.ts +++ b/x-pack/plugins/fleet/common/types/models/secret.ts @@ -4,10 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import type { PackagePolicyConfigRecordEntry } from '../..'; export interface Secret { id: string; - value: string; } export interface SecretElasticDoc { @@ -18,7 +17,19 @@ export interface VarSecretReference { id: string; isSecretRef: true; } +export interface SecretPath { + path: string; + value: PackagePolicyConfigRecordEntry; +} // this is used in the top level secret_refs array on package and agent policies export interface PolicySecretReference { id: string; } + +export interface DeletedSecretResponse { + deleted: boolean; +} +export interface DeletedSecretReference { + id: string; + deleted: boolean; +} diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts index 356aaa0e0cf8e..807eef8ba9917 100644 --- a/x-pack/plugins/fleet/server/constants/index.ts +++ b/x-pack/plugins/fleet/server/constants/index.ts @@ -78,7 +78,7 @@ export { // Message signing service MESSAGE_SIGNING_SERVICE_API_ROUTES, // secrets - SECRETS_INDEX, + SECRETS_ENDPOINT_PATH, } from '../../common/constants'; export { diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index c17f8f4ec2ea3..72fddd0e5b674 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -755,7 +755,6 @@ class PackagePolicyClientImpl implements PackagePolicyClient { packageInfo: pkgInfo, esClient, }); - restOfPackagePolicy = secretsRes.packagePolicyUpdate; secretReferences = secretsRes.secretReferences; secretsToDelete = secretsRes.secretsToDelete; diff --git a/x-pack/plugins/fleet/server/services/secrets.ts b/x-pack/plugins/fleet/server/services/secrets.ts index bd70e8435b02a..7e6efde6c11b0 100644 --- a/x-pack/plugins/fleet/server/services/secrets.ts +++ b/x-pack/plugins/fleet/server/services/secrets.ts @@ -6,19 +6,13 @@ */ import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; -import type { BulkResponse } from '@elastic/elasticsearch/lib/api/types'; -import { keyBy, partition } from 'lodash'; +import { keyBy } from 'lodash'; import { set } from '@kbn/safer-lodash-set'; import { packageHasNoPolicyTemplates } from '../../common/services/policy_template'; -import type { - NewPackagePolicy, - PackagePolicyConfigRecordEntry, - RegistryStream, - UpdatePackagePolicy, -} from '../../common'; +import type { NewPackagePolicy, RegistryStream, UpdatePackagePolicy } from '../../common'; import { SO_SEARCH_LIMIT } from '../../common'; import { @@ -34,75 +28,61 @@ import type { Secret, VarSecretReference, PolicySecretReference, + SecretPath, + DeletedSecretResponse, + DeletedSecretReference, } from '../types'; import { FleetError } from '../errors'; -import { SECRETS_INDEX } from '../constants'; +import { SECRETS_ENDPOINT_PATH } from '../constants'; + +import { retryTransientEsErrors } from './epm/elasticsearch/retry'; import { auditLoggingService } from './audit_logging'; import { appContextService } from './app_context'; import { packagePolicyService } from './package_policy'; -interface SecretPath { - path: string; - value: PackagePolicyConfigRecordEntry; -} - -// This will be removed once the secrets index PR is merged into elasticsearch -function getSecretsIndex() { - const testIndex = appContextService.getConfig()?.developer?.testSecretsIndex; - if (testIndex) { - return testIndex; - } - return SECRETS_INDEX; -} - export async function createSecrets(opts: { esClient: ElasticsearchClient; values: string[]; }): Promise { const { esClient, values } = opts; const logger = appContextService.getLogger(); - const body = values.flatMap((value) => [ - { - create: { _index: getSecretsIndex() }, - }, - { value }, - ]); - let res: BulkResponse; - try { - res = await esClient.bulk({ - body, - }); - const [errorItems, successItems] = partition(res.items, (a) => a.create?.error); + const secretsResponse: Secret[] = await Promise.all( + values.map(async (value) => { + try { + return await retryTransientEsErrors( + () => + esClient.transport.request({ + method: 'POST', + path: SECRETS_ENDPOINT_PATH, + body: { value }, + }), + { logger } + ); + } catch (err) { + const msg = `Error creating secrets: ${err}`; + logger.error(msg); + throw new FleetError(msg); + } + }) + ); - successItems.forEach((item) => { - auditLoggingService.writeCustomAuditLog({ - message: `secret created: ${item.create!._id}`, - event: { - action: 'secret_create', - category: ['database'], - type: ['access'], - outcome: 'success', - }, - }); + secretsResponse.forEach((item) => { + auditLoggingService.writeCustomAuditLog({ + message: `secret created: ${item.id}`, + event: { + action: 'secret_create', + category: ['database'], + type: ['access'], + outcome: 'success', + }, }); + }); - if (errorItems.length) { - throw new Error(JSON.stringify(errorItems)); - } - - return res.items.map((item, i) => ({ - id: item.create!._id as string, - value: values[i], - })); - } catch (e) { - const msg = `Error creating secrets in ${getSecretsIndex()} index: ${e}`; - logger.error(msg); - throw new FleetError(msg); - } + return secretsResponse; } export async function deleteSecretsIfNotReferenced(opts: { @@ -190,24 +170,32 @@ export async function _deleteSecrets(opts: { }): Promise { const { esClient, ids } = opts; const logger = appContextService.getLogger(); - const body = ids.flatMap((id) => [ - { - delete: { _index: getSecretsIndex(), _id: id }, - }, - ]); - - let res: BulkResponse; - - try { - res = await esClient.bulk({ - body, - }); - const [errorItems, successItems] = partition(res.items, (a) => a.delete?.error); + const deletedRes: DeletedSecretReference[] = await Promise.all( + ids.map(async (id) => { + try { + const getDeleteRes: DeletedSecretResponse = await retryTransientEsErrors( + () => + esClient.transport.request({ + method: 'DELETE', + path: `${SECRETS_ENDPOINT_PATH}/${id}`, + }), + { logger } + ); + + return { ...getDeleteRes, id }; + } catch (err) { + const msg = `Error deleting secrets: ${err}`; + logger.error(msg); + throw new FleetError(msg); + } + }) + ); - successItems.forEach((item) => { + deletedRes.forEach((item) => { + if (item.deleted === true) { auditLoggingService.writeCustomAuditLog({ - message: `secret deleted: ${item.delete!._id}`, + message: `secret deleted: ${item.id}`, event: { action: 'secret_delete', category: ['database'], @@ -215,16 +203,8 @@ export async function _deleteSecrets(opts: { outcome: 'success', }, }); - }); - - if (errorItems.length) { - throw new Error(JSON.stringify(errorItems)); } - } catch (e) { - const msg = `Error deleting secrets from ${getSecretsIndex()} index: ${e}`; - logger.error(msg); - throw new FleetError(msg); - } + }); } export async function extractAndWriteSecrets(opts: { diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx index ec1bde6292770..b9dc7528651eb 100644 --- a/x-pack/plugins/fleet/server/types/index.tsx +++ b/x-pack/plugins/fleet/server/types/index.tsx @@ -85,8 +85,11 @@ export type { ExperimentalDataStreamFeature, Secret, SecretElasticDoc, + SecretPath, VarSecretReference, PolicySecretReference, + DeletedSecretResponse, + DeletedSecretReference, PackageListItem, PackageList, InstallationInfo, diff --git a/x-pack/plugins/logs_shared/server/services/log_entries/log_entries_search_strategy.test.ts b/x-pack/plugins/logs_shared/server/services/log_entries/log_entries_search_strategy.test.ts index bb21053cfe9d8..305f6292deb28 100644 --- a/x-pack/plugins/logs_shared/server/services/log_entries/log_entries_search_strategy.test.ts +++ b/x-pack/plugins/logs_shared/server/services/log_entries/log_entries_search_strategy.test.ts @@ -298,6 +298,7 @@ const createSearchStrategyDependenciesMock = (): SearchStrategyDependencies => ( savedObjectsClient: savedObjectsClientMock.create(), searchSessionsClient: createSearchSessionsClientMock(), request: httpServerMock.createKibanaRequest(), + rollupsEnabled: true, }); // using the official data mock from within x-pack doesn't type-check successfully, diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/add_inference_pipeline_flyout.tsx b/x-pack/plugins/ml/public/application/components/ml_inference/add_inference_pipeline_flyout.tsx new file mode 100644 index 0000000000000..7d4ea408111fe --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/ml_inference/add_inference_pipeline_flyout.tsx @@ -0,0 +1,186 @@ +/* + * 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, { FC, useMemo, useState } from 'react'; + +import { + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiFlyoutFooter, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { extractErrorProperties } from '@kbn/ml-error-utils'; + +import { ModelItem } from '../../model_management/models_list'; +import type { AddInferencePipelineSteps } from './types'; +import { ADD_INFERENCE_PIPELINE_STEPS } from './constants'; +import { AddInferencePipelineFooter } from './components/add_inference_pipeline_footer'; +import { AddInferencePipelineHorizontalSteps } from './components/add_inference_pipeline_horizontal_steps'; +import { getInitialState, getModelType } from './state'; +import { PipelineDetails } from './components/pipeline_details'; +import { ProcessorConfiguration } from './components/processor_configuration'; +import { OnFailureConfiguration } from './components/on_failure_configuration'; +import { TestPipeline } from './components/test_pipeline'; +import { ReviewAndCreatePipeline } from './components/review_and_create_pipeline'; +import { useMlApiContext } from '../../contexts/kibana'; +import { getPipelineConfig } from './get_pipeline_config'; +import { validateInferencePipelineConfigurationStep } from './validation'; +import type { MlInferenceState, InferenceModelTypes } from './types'; +import { useFetchPipelines } from './hooks/use_fetch_pipelines'; + +export interface AddInferencePipelineFlyoutProps { + onClose: () => void; + model: ModelItem; +} + +export const AddInferencePipelineFlyout: FC = ({ + onClose, + model, +}) => { + // eslint-disable-next-line react-hooks/exhaustive-deps + const initialState = useMemo(() => getInitialState(model), [model.model_id]); + const [formState, setFormState] = useState(initialState); + const [step, setStep] = useState(ADD_INFERENCE_PIPELINE_STEPS.DETAILS); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); + + const { + trainedModels: { createInferencePipeline }, + } = useMlApiContext(); + + const modelType = getModelType(model); + + const createPipeline = async () => { + setFormState({ ...formState, creatingPipeline: true }); + try { + await createInferencePipeline(formState.pipelineName, getPipelineConfig(formState)); + setFormState({ + ...formState, + pipelineCreated: true, + creatingPipeline: false, + pipelineError: undefined, + }); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + const errorProperties = extractErrorProperties(e); + setFormState({ + ...formState, + creatingPipeline: false, + pipelineError: errorProperties.message ?? e.message, + }); + } + }; + + const pipelineNames = useFetchPipelines(); + + const handleConfigUpdate = (configUpdate: Partial) => { + setFormState({ ...formState, ...configUpdate }); + }; + + const { pipelineName: pipelineNameError, targetField: targetFieldError } = useMemo(() => { + const errors = validateInferencePipelineConfigurationStep( + formState.pipelineName, + pipelineNames + ); + return errors; + }, [pipelineNames, formState.pipelineName]); + + const sourceIndex = useMemo( + () => + Array.isArray(model.metadata?.analytics_config.source.index) + ? model.metadata?.analytics_config.source.index.join() + : model.metadata?.analytics_config.source.index, + // eslint-disable-next-line react-hooks/exhaustive-deps + [model?.model_id] + ); + + return ( + + + +

+ {i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.title', + { + defaultMessage: 'Deploy analytics model', + } + )} +

+
+
+ + + + {step === ADD_INFERENCE_PIPELINE_STEPS.DETAILS && ( + + )} + {step === ADD_INFERENCE_PIPELINE_STEPS.CONFIGURE_PROCESSOR && model && ( + + )} + {step === ADD_INFERENCE_PIPELINE_STEPS.ON_FAILURE && ( + + )} + {step === ADD_INFERENCE_PIPELINE_STEPS.TEST && ( + + )} + {step === ADD_INFERENCE_PIPELINE_STEPS.CREATE && ( + + )} + + + + +
+ ); +}; diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/components/add_inference_pipeline_footer.tsx b/x-pack/plugins/ml/public/application/components/ml_inference/components/add_inference_pipeline_footer.tsx new file mode 100644 index 0000000000000..f0a8beb2482f6 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/ml_inference/components/add_inference_pipeline_footer.tsx @@ -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 React, { FC, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { AddInferencePipelineSteps } from '../types'; +import { + BACK_BUTTON_LABEL, + CANCEL_BUTTON_LABEL, + CLOSE_BUTTON_LABEL, + CONTINUE_BUTTON_LABEL, +} from '../constants'; +import { getSteps } from '../get_steps'; + +interface Props { + isDetailsStepValid: boolean; + isConfigureProcessorStepValid: boolean; + pipelineCreated: boolean; + creatingPipeline: boolean; + step: AddInferencePipelineSteps; + onClose: () => void; + onCreate: () => void; + setStep: React.Dispatch>; +} + +export const AddInferencePipelineFooter: FC = ({ + isDetailsStepValid, + isConfigureProcessorStepValid, + creatingPipeline, + pipelineCreated, + onClose, + onCreate, + step, + setStep, +}) => { + const { nextStep, previousStep, isContinueButtonEnabled } = useMemo( + () => getSteps(step, isDetailsStepValid, isConfigureProcessorStepValid), + [isDetailsStepValid, isConfigureProcessorStepValid, step] + ); + + return ( + + + + {pipelineCreated ? CLOSE_BUTTON_LABEL : CANCEL_BUTTON_LABEL} + + + + + {previousStep !== undefined && pipelineCreated === false ? ( + setStep(previousStep as AddInferencePipelineSteps)} + > + {BACK_BUTTON_LABEL} + + ) : null} + + + {nextStep !== undefined ? ( + setStep(nextStep as AddInferencePipelineSteps)} + disabled={!isContinueButtonEnabled} + fill + > + {CONTINUE_BUTTON_LABEL} + + ) : ( + + {i18n.translate( + 'xpack.ml.trainedModels.content.indices.addInferencePipelineModal.footer.create', + { + defaultMessage: 'Create pipeline', + } + )} + + )} + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/components/add_inference_pipeline_horizontal_steps.tsx b/x-pack/plugins/ml/public/application/components/ml_inference/components/add_inference_pipeline_horizontal_steps.tsx new file mode 100644 index 0000000000000..9954ed8955259 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/ml_inference/components/add_inference_pipeline_horizontal_steps.tsx @@ -0,0 +1,118 @@ +/* + * 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, { FC, memo } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { EuiStepsHorizontal, EuiStepsHorizontalProps } from '@elastic/eui'; +import type { AddInferencePipelineSteps } from '../types'; +import { ADD_INFERENCE_PIPELINE_STEPS } from '../constants'; + +const steps = Object.values(ADD_INFERENCE_PIPELINE_STEPS); + +interface Props { + step: AddInferencePipelineSteps; + setStep: React.Dispatch>; + isDetailsStepValid: boolean; + isConfigureProcessorStepValid: boolean; +} + +export const AddInferencePipelineHorizontalSteps: FC = memo( + ({ step, setStep, isDetailsStepValid, isConfigureProcessorStepValid }) => { + const currentStepIndex = steps.findIndex((s) => s === step); + const navSteps: EuiStepsHorizontalProps['steps'] = [ + { + // Details + onClick: () => setStep(ADD_INFERENCE_PIPELINE_STEPS.DETAILS), + status: isDetailsStepValid ? 'complete' : 'incomplete', + title: i18n.translate( + 'xpack.ml.inferencePipeline.content.indices.transforms.addInferencePipelineModal.steps.details.title', + { + defaultMessage: 'Details', + } + ), + }, + { + // Processor configuration + onClick: () => { + if (!isDetailsStepValid) return; + setStep(ADD_INFERENCE_PIPELINE_STEPS.CONFIGURE_PROCESSOR); + }, + status: + isDetailsStepValid && isConfigureProcessorStepValid && currentStepIndex > 1 + ? 'complete' + : 'incomplete', + title: i18n.translate( + 'xpack.ml.inferencePipeline.content.indices.transforms.addInferencePipelineModal.steps.configureProcessor.title', + { + defaultMessage: 'Configure processor', + } + ), + }, + { + // handle failures + onClick: () => { + if (!isDetailsStepValid) return; + setStep(ADD_INFERENCE_PIPELINE_STEPS.ON_FAILURE); + }, + status: currentStepIndex > 2 ? 'complete' : 'incomplete', + title: i18n.translate( + 'xpack.ml.inferencePipeline.content.indices.transforms.addInferencePipelineModal.steps.handleFailures.title', + { + defaultMessage: 'Handle failures', + } + ), + }, + { + // Test + onClick: () => { + if (!isConfigureProcessorStepValid || !isDetailsStepValid) return; + setStep(ADD_INFERENCE_PIPELINE_STEPS.TEST); + }, + status: currentStepIndex > 3 ? 'complete' : 'incomplete', + title: i18n.translate( + 'xpack.ml.trainedModels.content.indices.transforms.addInferencePipelineModal.steps.test.title', + { + defaultMessage: 'Test (Optional)', + } + ), + }, + { + // Review and Create + onClick: () => { + if (!isConfigureProcessorStepValid) return; + setStep(ADD_INFERENCE_PIPELINE_STEPS.CREATE); + }, + status: isDetailsStepValid && isConfigureProcessorStepValid ? 'incomplete' : 'disabled', + title: i18n.translate( + 'xpack.ml.inferencePipeline.content.indices.transforms.addInferencePipelineModal.steps.create.title', + { + defaultMessage: 'Create', + } + ), + }, + ]; + switch (step) { + case ADD_INFERENCE_PIPELINE_STEPS.DETAILS: + navSteps[0].status = 'current'; + break; + case ADD_INFERENCE_PIPELINE_STEPS.CONFIGURE_PROCESSOR: + navSteps[1].status = 'current'; + break; + case ADD_INFERENCE_PIPELINE_STEPS.ON_FAILURE: + navSteps[2].status = 'current'; + break; + case ADD_INFERENCE_PIPELINE_STEPS.TEST: + navSteps[3].status = 'current'; + break; + case ADD_INFERENCE_PIPELINE_STEPS.CREATE: + navSteps[4].status = 'current'; + break; + } + return ; + } +); diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/components/additional_advanced_settings.tsx b/x-pack/plugins/ml/public/application/components/ml_inference/components/additional_advanced_settings.tsx new file mode 100644 index 0000000000000..4b3cfbfcf795b --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/ml_inference/components/additional_advanced_settings.tsx @@ -0,0 +1,162 @@ +/* + * 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, { FC, useState, memo, useMemo } from 'react'; + +import { + EuiAccordion, + EuiFlexGroup, + EuiFieldText, + EuiFlexItem, + EuiFormRow, + EuiLink, + EuiPanel, + EuiTextArea, + htmlIdGenerator, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { AdditionalSettings, MlInferenceState } from '../types'; +import { SaveChangesButton } from './save_changes_button'; +import { useMlKibana } from '../../../contexts/kibana'; + +interface Props { + condition?: string; + tag?: string; + handleAdvancedConfigUpdate: (configUpdate: Partial) => void; +} + +export const AdditionalAdvancedSettings: FC = memo( + ({ handleAdvancedConfigUpdate, condition, tag }) => { + const [additionalSettings, setAdditionalSettings] = useState< + Partial | undefined + >(condition || tag ? { condition, tag } : undefined); + + const { + services: { + docLinks: { links }, + }, + } = useMlKibana(); + + const handleAdditionalSettingsChange = (settingsChange: Partial) => { + setAdditionalSettings({ ...additionalSettings, ...settingsChange }); + }; + + const accordionId = useMemo(() => htmlIdGenerator()(), []); + const additionalSettingsUpdated = useMemo( + () => additionalSettings?.tag !== tag || additionalSettings?.condition !== condition, + [additionalSettings, tag, condition] + ); + + const updateAdditionalSettings = () => { + handleAdvancedConfigUpdate({ ...additionalSettings }); + }; + + return ( + + + + + + {additionalSettingsUpdated ? ( + + ) : null} + + + } + > + + + {/* CONDITION */} + + + } + helpText={ + + Painless + + ), + }} + /> + } + > + ) => + handleAdditionalSettingsChange({ condition: e.target.value }) + } + /> + + + {/* TAG */} + + + + + } + helpText={ + + } + > + ) => + handleAdditionalSettingsChange({ tag: e.target.value }) + } + aria-label={i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.tagAriaLabel', + { defaultMessage: 'Optional tag identifier for the processor' } + )} + /> + + + + + + + + ); + } +); diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/components/on_failure_configuration.tsx b/x-pack/plugins/ml/public/application/components/ml_inference/components/on_failure_configuration.tsx new file mode 100644 index 0000000000000..bc8bc4eedb2d3 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/ml_inference/components/on_failure_configuration.tsx @@ -0,0 +1,270 @@ +/* + * 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, { FC, useState, memo } from 'react'; + +import { + EuiButtonEmpty, + EuiCode, + EuiCodeBlock, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiLink, + EuiSpacer, + EuiSwitch, + EuiSwitchEvent, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { CodeEditor } from '@kbn/kibana-react-plugin/public'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { SaveChangesButton } from './save_changes_button'; +import type { MlInferenceState } from '../types'; +import { getDefaultOnFailureConfiguration } from '../state'; +import { CANCEL_EDIT_MESSAGE, EDIT_MESSAGE } from '../constants'; +import { useMlKibana } from '../../../contexts/kibana'; +import { isValidJson } from '../../../../../common/util/validation_utils'; + +interface Props { + handleAdvancedConfigUpdate: (configUpdate: Partial) => void; + ignoreFailure: boolean; + onFailure: MlInferenceState['onFailure']; + takeActionOnFailure: MlInferenceState['takeActionOnFailure']; +} + +export const OnFailureConfiguration: FC = memo( + ({ handleAdvancedConfigUpdate, ignoreFailure, onFailure, takeActionOnFailure }) => { + const { + services: { + docLinks: { links }, + }, + } = useMlKibana(); + + const [editOnFailure, setEditOnFailure] = useState(false); + const [isOnFailureValid, setIsOnFailureValid] = useState(false); + const [onFailureString, setOnFailureString] = useState( + JSON.stringify(onFailure, null, 2) + ); + + const updateIgnoreFailure = (e: EuiSwitchEvent) => { + const checked = e.target.checked; + handleAdvancedConfigUpdate({ + ignoreFailure: checked, + ...(checked === true ? { takeActionOnFailure: false, onFailure: undefined } : {}), + }); + }; + + const updateOnFailure = () => { + handleAdvancedConfigUpdate({ onFailure: JSON.parse(onFailureString) }); + setEditOnFailure(false); + }; + + const handleOnFailureChange = (json: string) => { + setOnFailureString(json); + const valid = isValidJson(json); + setIsOnFailureValid(valid); + }; + + const handleTakeActionOnFailureChange = (checked: boolean) => { + handleAdvancedConfigUpdate({ + takeActionOnFailure: checked, + onFailure: checked === false ? undefined : getDefaultOnFailureConfiguration(), + }); + if (checked === false) { + setEditOnFailure(false); + setIsOnFailureValid(true); + } + }; + + const resetOnFailure = () => { + setOnFailureString(JSON.stringify(getDefaultOnFailureConfiguration(), null, 2)); + setIsOnFailureValid(true); + }; + + return ( + + + + + +

+ {i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.onFailureTitle', + { defaultMessage: 'Ingesting problematic documents' } + )} +

+
+ + +

+ +

+

+ {'ignore_failure'}, + inferenceDocsLink: ( + + Learn more. + + ), + }} + /> +

+

+ {'on_failure'}, + onFailureDocsLink: ( + + Learn more. + + ), + }} + /> +

+
+
+ + + + + + + + } + checked={ignoreFailure} + onChange={updateIgnoreFailure} + /> + + + + {ignoreFailure === false ? ( + + + + + } + checked={takeActionOnFailure} + onChange={(e: EuiSwitchEvent) => + handleTakeActionOnFailureChange(e.target.checked) + } + /> + + ) : null} + + + + + {takeActionOnFailure === true && ignoreFailure === false ? ( + + + {i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.onFailureHeadingLabel', + { defaultMessage: 'Actions to take on failure' } + )} + + + } + labelAppend={ + + + { + setEditOnFailure(!editOnFailure); + }} + > + {editOnFailure ? CANCEL_EDIT_MESSAGE : EDIT_MESSAGE} + + + + {editOnFailure ? ( + + ) : null} + + + {editOnFailure ? ( + + {i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.resetOnFailureButton', + { defaultMessage: 'Reset' } + )} + + ) : null} + + + } + helpText={ + + } + > + <> + {!editOnFailure ? ( + + {JSON.stringify(onFailure, null, 2)} + + ) : null} + {editOnFailure ? ( + + ) : null} + + + ) : null} + +
+
+
+ + + ); + } +); diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/components/pipeline_details.tsx b/x-pack/plugins/ml/public/application/components/ml_inference/components/pipeline_details.tsx new file mode 100644 index 0000000000000..4988c772c2863 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/ml_inference/components/pipeline_details.tsx @@ -0,0 +1,223 @@ +/* + * 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, { FC, memo } from 'react'; + +import { + EuiCode, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiLink, + EuiSpacer, + EuiTitle, + EuiText, + EuiTextArea, + EuiPanel, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useMlKibana } from '../../../contexts/kibana'; +import type { MlInferenceState } from '../types'; + +interface Props { + handlePipelineConfigUpdate: (configUpdate: Partial) => void; + modelId: string; + pipelineNameError: string | undefined; + pipelineName: string; + pipelineDescription: string; + targetField: string; + targetFieldError: string | undefined; +} + +export const PipelineDetails: FC = memo( + ({ + handlePipelineConfigUpdate, + modelId, + pipelineName, + pipelineNameError, + pipelineDescription, + targetField, + targetFieldError, + }) => { + const { + services: { + docLinks: { links }, + }, + } = useMlKibana(); + + const handleConfigChange = (value: string, type: string) => { + handlePipelineConfigUpdate({ [type]: value }); + }; + + return ( + + + +

+ {i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.title', + { defaultMessage: 'Create a pipeline' } + )} +

+
+ + +

+ {modelId}, + pipeline: ( + + pipeline + + ), + }} + /> +

+

+ + _reindex API + + ), + pipelineSimulateLink: ( + + pipeline/_simulate + + ), + }} + /> +

+
+
+ + + {/* NAME */} + + + {i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.name.helpText', + { + defaultMessage: + 'Pipeline names are unique within a deployment and can only contain letters, numbers, underscores, and hyphens.', + } + )} + + ) + } + error={pipelineNameError} + isInvalid={pipelineNameError !== undefined} + > + ) => + handleConfigChange(e.target.value, 'pipelineName') + } + /> + + {/* DESCRIPTION */} + + {i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.description.helpText', + { + defaultMessage: 'A description of what this pipeline does.', + } + )} + + } + > + ) => + handleConfigChange(e.target.value, 'pipelineDescription') + } + /> + + {/* TARGET FIELD */} + {'ml.inference.'} }} + /> + ) + } + error={targetFieldError} + isInvalid={targetFieldError !== undefined} + > + ) => + handleConfigChange(e.target.value, 'targetField') + } + /> + + + + +
+ ); + } +); diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/components/processor_configuration.tsx b/x-pack/plugins/ml/public/application/components/ml_inference/components/processor_configuration.tsx new file mode 100644 index 0000000000000..7f2dfe9ede728 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/ml_inference/components/processor_configuration.tsx @@ -0,0 +1,391 @@ +/* + * 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, { FC, useState, memo } from 'react'; + +import { + EuiButtonEmpty, + EuiCode, + EuiCodeBlock, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiLink, + EuiSpacer, + EuiText, + EuiTitle, + EuiPopover, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { CodeEditor } from '@kbn/kibana-react-plugin/public'; +import { ModelItem } from '../../../model_management/models_list'; +import { + EDIT_MESSAGE, + CANCEL_EDIT_MESSAGE, + CREATE_FIELD_MAPPING_MESSAGE, + CLEAR_BUTTON_LABEL, +} from '../constants'; +import { validateInferenceConfig } from '../validation'; +import { isValidJson } from '../../../../../common/util/validation_utils'; +import { SaveChangesButton } from './save_changes_button'; +import { useMlKibana } from '../../../contexts/kibana'; +import type { MlInferenceState, InferenceModelTypes } from '../types'; +import { AdditionalAdvancedSettings } from './additional_advanced_settings'; +import { validateFieldMap } from '../validation'; + +function getDefaultFieldMapString() { + return JSON.stringify( + { + field_map: { + incoming_field: 'field_the_model_expects', + }, + }, + null, + 2 + ); +} + +interface Props { + condition?: string; + fieldMap: MlInferenceState['fieldMap']; + handleAdvancedConfigUpdate: (configUpdate: Partial) => void; + inferenceConfig: ModelItem['inference_config']; + modelInferenceConfig: ModelItem['inference_config']; + modelInputFields: ModelItem['input']; + modelType?: InferenceModelTypes; + setHasUnsavedChanges: React.Dispatch>; + tag?: string; +} + +export const ProcessorConfiguration: FC = memo( + ({ + condition, + fieldMap, + handleAdvancedConfigUpdate, + inferenceConfig, + modelInputFields, + modelInferenceConfig, + modelType, + setHasUnsavedChanges, + tag, + }) => { + const { + services: { + docLinks: { links }, + }, + } = useMlKibana(); + + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [editInferenceConfig, setEditInferenceConfig] = useState(false); + const [editFieldMapping, setEditFieldMapping] = useState(false); + const [inferenceConfigString, setInferenceConfigString] = useState( + JSON.stringify(inferenceConfig, null, 2) + ); + const [inferenceConfigError, setInferenceConfigError] = useState(); + const [fieldMapError, setFieldMapError] = useState(); + const [fieldMappingString, setFieldMappingString] = useState( + fieldMap ? JSON.stringify(fieldMap, null, 2) : getDefaultFieldMapString() + ); + const [isInferenceConfigValid, setIsInferenceConfigValid] = useState(true); + const [isFieldMapValid, setIsFieldMapValid] = useState(true); + + const handleInferenceConfigChange = (json: string) => { + setInferenceConfigString(json); + const valid = isValidJson(json); + setIsInferenceConfigValid(valid); + }; + + const updateInferenceConfig = () => { + const invalidInferenceConfigMessage = validateInferenceConfig( + JSON.parse(inferenceConfigString), + modelType + ); + + if (invalidInferenceConfigMessage === undefined) { + handleAdvancedConfigUpdate({ inferenceConfig: JSON.parse(inferenceConfigString) }); + setHasUnsavedChanges(false); + setEditInferenceConfig(false); + setInferenceConfigError(undefined); + } else { + setHasUnsavedChanges(true); + setIsInferenceConfigValid(false); + setInferenceConfigError(invalidInferenceConfigMessage); + } + }; + + const resetInferenceConfig = () => { + setInferenceConfigString(JSON.stringify(modelInferenceConfig, null, 2)); + setIsInferenceConfigValid(true); + setInferenceConfigError(undefined); + }; + + const clearFieldMap = () => { + setFieldMappingString('{}'); + setIsFieldMapValid(true); + setFieldMapError(undefined); + }; + + const handleFieldMapChange = (json: string) => { + setFieldMappingString(json); + const valid = isValidJson(json); + setIsFieldMapValid(valid); + }; + + const updateFieldMap = () => { + const invalidFieldMapMessage = validateFieldMap( + modelInputFields.field_names ?? [], + JSON.parse(fieldMappingString) + ); + + if (invalidFieldMapMessage === undefined) { + handleAdvancedConfigUpdate({ fieldMap: JSON.parse(fieldMappingString) }); + setHasUnsavedChanges(false); + setEditFieldMapping(false); + setFieldMapError(undefined); + } else { + setHasUnsavedChanges(true); + setIsFieldMapValid(false); + setFieldMapError(invalidFieldMapMessage); + } + }; + + return ( + + {/* INFERENCE CONFIG */} + + + + +

+ {i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.inferenceConfigurationTitle', + { defaultMessage: 'Inference configuration' } + )} +

+
+ + +

+ + Learn more. + + ), + }} + /> +

+
+
+ + + + { + if (!editInferenceConfig === false) { + setInferenceConfigError(undefined); + setIsInferenceConfigValid(true); + } + setEditInferenceConfig(!editInferenceConfig); + }} + > + {editInferenceConfig ? CANCEL_EDIT_MESSAGE : EDIT_MESSAGE} + + + + {editInferenceConfig ? ( + + ) : null} + + + {editInferenceConfig ? ( + + {i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.resetInferenceConfigButton', + { defaultMessage: 'Reset' } + )} + + ) : null} + +
+ } + error={inferenceConfigError ?? inferenceConfigError} + isInvalid={inferenceConfigError !== undefined || inferenceConfigError !== undefined} + > + {editInferenceConfig ? ( + + ) : ( + + {JSON.stringify(inferenceConfig, null, 2)} + + )} + +
+
+ + {/* FIELD MAP */} + + + + +

+ {i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.fieldMapTitle', + { defaultMessage: 'Fields' } + )} +

+
+ + +

+ setIsPopoverOpen(!isPopoverOpen)}> + You can review them here. + + } + isOpen={isPopoverOpen} + closePopover={() => setIsPopoverOpen(false)} + anchorPosition="downLeft" + > + + {JSON.stringify(modelInputFields, null, 2)} + + + ), + }} + /> +

+

+ {'field_map'}, + inferenceDocsLink: ( + + Learn more. + + ), + }} + /> +

+
+
+ + + + { + const editingState = !editFieldMapping; + if (editingState === false) { + setFieldMapError(undefined); + setIsFieldMapValid(true); + setHasUnsavedChanges(false); + } + setEditFieldMapping(editingState); + }} + > + {editFieldMapping + ? CANCEL_EDIT_MESSAGE + : fieldMap !== undefined + ? EDIT_MESSAGE + : CREATE_FIELD_MAPPING_MESSAGE} + + + {editFieldMapping ? ( + + + + ) : null} + {editFieldMapping ? ( + + + {CLEAR_BUTTON_LABEL} + + + ) : null} +
+ } + error={fieldMapError} + isInvalid={fieldMapError !== undefined} + > + <> + {!editFieldMapping ? ( + + {JSON.stringify(fieldMap ?? {}, null, 2)} + + ) : null} + {editFieldMapping ? ( + <> + + + + ) : null} + + +
+ + + {/* ADDITIONAL ADVANCED SETTINGS */} + + + + + ); + } +); diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/components/review_and_create_pipeline.tsx b/x-pack/plugins/ml/public/application/components/ml_inference/components/review_and_create_pipeline.tsx new file mode 100644 index 0000000000000..352f11a0ba867 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/ml_inference/components/review_and_create_pipeline.tsx @@ -0,0 +1,214 @@ +/* + * 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, { FC, useMemo } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { + EuiAccordion, + EuiCallOut, + EuiCodeBlock, + EuiFlexGroup, + EuiFlexItem, + EuiLink, + EuiSpacer, + EuiTitle, + EuiText, + htmlIdGenerator, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { IngestPipeline } from '@elastic/elasticsearch/lib/api/types'; +import { useMlKibana } from '../../../contexts/kibana'; + +const MANAGEMENT_APP_ID = 'management'; + +interface Props { + inferencePipeline: IngestPipeline; + modelType?: string; + pipelineName: string; + pipelineCreated: boolean; + pipelineError?: string; +} + +export const ReviewAndCreatePipeline: FC = ({ + inferencePipeline, + modelType, + pipelineName, + pipelineCreated, + pipelineError, +}) => { + const { + services: { + application, + docLinks: { links }, + }, + } = useMlKibana(); + + const inferenceProcessorLink = + modelType === 'regression' + ? links.ingest.inferenceRegression + : links.ingest.inferenceClassification; + + const accordionId = useMemo(() => htmlIdGenerator()(), []); + + const configCodeBlock = useMemo( + () => ( + + {JSON.stringify(inferencePipeline ?? {}, null, 2)} + + ), + [inferencePipeline] + ); + + return ( + <> + + + {pipelineCreated === false ? ( + +

+ {i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.review.title', + { + defaultMessage: "Review the pipeline configuration for '{pipelineName}'", + values: { pipelineName }, + } + )} +

+
+ ) : null} + <> + + {pipelineCreated === true && pipelineError === undefined ? ( + +

+ + {'reindexing'} + + ), + }} + /> + {application.capabilities.management?.ingest?.ingest_pipelines ? ( + { + await application.navigateToApp(MANAGEMENT_APP_ID, { + path: `/ingest/ingest_pipelines/?pipeline=${pipelineName}`, + openInNewTab: true, + }); + }} + target="_blank" + external + > + {'Ingest Pipelines'} + + ), + }} + /> + ) : null} +

+
+ ) : null} + {pipelineError !== undefined ? ( + +

{pipelineError}

+

+ + {'ingest pipeline'} + + ), + inferencePipelineConfigLink: ( + + {'inference processor'} + + ), + }} + /> +

+
+ ) : null} + +
+ + +

+ {!pipelineCreated ? ( + + ) : null} +

+
+
+ + {pipelineCreated ? ( + <> + + + } + > + {configCodeBlock} + + + ) : ( + [configCodeBlock] + )} + +
+ + ); +}; diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/components/save_changes_button.tsx b/x-pack/plugins/ml/public/application/components/ml_inference/components/save_changes_button.tsx new file mode 100644 index 0000000000000..73e763d153799 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/ml_inference/components/save_changes_button.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, { FC } from 'react'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +interface SaveChangesButtonProps { + onClick: () => void; + disabled: boolean; +} + +export const SaveChangesButton: FC = ({ onClick, disabled }) => ( + + {i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.saveChangesButton', + { defaultMessage: 'Save changes' } + )} + +); diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/components/test_pipeline.tsx b/x-pack/plugins/ml/public/application/components/ml_inference/components/test_pipeline.tsx new file mode 100644 index 0000000000000..19120de441f4c --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/ml_inference/components/test_pipeline.tsx @@ -0,0 +1,279 @@ +/* + * 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, { FC, memo, useEffect, useCallback, useState } from 'react'; +import { css } from '@emotion/react'; +import { euiThemeVars } from '@kbn/ui-theme'; +import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; + +import { + EuiButton, + EuiButtonEmpty, + EuiCode, + EuiCodeBlock, + EuiFlexGroup, + EuiFlexItem, + EuiResizableContainer, + EuiSpacer, + EuiTitle, + EuiText, + useIsWithinMaxBreakpoint, + EuiPanel, +} from '@elastic/eui'; + +import { IngestSimulateDocument } from '@elastic/elasticsearch/lib/api/types'; +import { extractErrorProperties } from '@kbn/ml-error-utils'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { CodeEditor } from '@kbn/kibana-react-plugin/public'; +import { useMlApiContext, useMlKibana } from '../../../contexts/kibana'; +import { getPipelineConfig } from '../get_pipeline_config'; +import { isValidJson } from '../../../../../common/util/validation_utils'; +import type { MlInferenceState } from '../types'; + +interface Props { + sourceIndex?: string; + state: MlInferenceState; +} + +export const TestPipeline: FC = memo(({ state, sourceIndex }) => { + const [simulatePipelineResult, setSimulatePipelineResult] = useState< + undefined | estypes.IngestSimulateResponse + >(); + const [simulatePipelineError, setSimulatePipelineError] = useState(); + const [sampleDocsString, setSampleDocsString] = useState(''); + const [isValid, setIsValid] = useState(true); + const { + esSearch, + trainedModels: { trainedModelPipelineSimulate }, + } = useMlApiContext(); + const { + notifications: { toasts }, + } = useMlKibana(); + + const isSmallerViewport = useIsWithinMaxBreakpoint('s'); + + const simulatePipeline = async () => { + try { + const pipelineConfig = getPipelineConfig(state); + const result = await trainedModelPipelineSimulate( + pipelineConfig, + JSON.parse(sampleDocsString) as IngestSimulateDocument[] + ); + setSimulatePipelineResult(result); + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + const errorProperties = extractErrorProperties(error); + setSimulatePipelineError(error); + toasts.danger({ + title: i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.test.errorSimulatingPipeline', + { + defaultMessage: 'Unable to simulate pipeline.', + } + ), + body: errorProperties.message, + toastLifeTimeMs: 5000, + }); + } + }; + + const clearResults = () => { + setSimulatePipelineResult(undefined); + setSimulatePipelineError(undefined); + }; + + const onChange = (json: string) => { + setSampleDocsString(json); + const valid = isValidJson(json); + setIsValid(valid); + }; + + const getSampleDocs = useCallback(async () => { + let records: IngestSimulateDocument[] = []; + let resp; + + try { + resp = await esSearch({ + index: sourceIndex, + body: { + size: 1, + }, + }); + + if (resp && resp.hits.total.value > 0) { + records = resp.hits.hits; + } + } catch (error) { + // eslint-disable-next-line no-console + console.error(error); + } + setSampleDocsString(JSON.stringify(records, null, 2)); + setIsValid(true); + }, [sourceIndex, esSearch]); + + useEffect( + function fetchSampleDocsFromSource() { + if (sourceIndex) { + getSampleDocs(); + } + }, + [sourceIndex, getSampleDocs] + ); + + return ( + <> + + + +

+ {i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.test.title', + { defaultMessage: 'Test the pipeline results' } + )} +

+
+
+ + +

+ + {i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.test.optionalCallout', + { defaultMessage: 'This is an optional step.' } + )} + +   + {' '} + {state.targetField && ( + {state.targetField} }} + /> + )} +

+
+
+
+ + + + + + +
+ + {i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.test.runButton', + { defaultMessage: 'Simulate pipeline' } + )} + +
+
+ + + {i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.test.clearResultsButton', + { defaultMessage: 'Clear results' } + )} + + + + + {i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.test.resetSampleDocsButton', + { defaultMessage: 'Reset sample docs' } + )} + + +
+ +
+ + + + +
+ {i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.test.subtitle.documents', + { defaultMessage: 'Raw document' } + )} +
+
+
+ + +
+ {i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.test.subtitle.result', + { defaultMessage: 'Result' } + )} +
+
+
+
+
+ + + {(EuiResizablePanel, EuiResizableButton) => ( + <> + + + + + + + + + {simulatePipelineError + ? JSON.stringify(simulatePipelineError, null, 2) + : simulatePipelineResult + ? JSON.stringify(simulatePipelineResult, null, 2) + : '{}'} + + + + )} + + + +
+
+ + ); +}); diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/constants.ts b/x-pack/plugins/ml/public/application/components/ml_inference/constants.ts new file mode 100644 index 0000000000000..8bf26c55b1b6d --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/ml_inference/constants.ts @@ -0,0 +1,61 @@ +/* + * 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 ADD_INFERENCE_PIPELINE_STEPS = { + DETAILS: 'Details', + CONFIGURE_PROCESSOR: 'Configure processor', + ON_FAILURE: 'Failure handling', + TEST: 'Test', + CREATE: 'create', +} as const; + +export const CANCEL_BUTTON_LABEL = i18n.translate( + 'xpack.ml.trainedModels.actions.cancelButtonLabel', + { defaultMessage: 'Cancel' } +); + +export const CLEAR_BUTTON_LABEL = i18n.translate( + 'xpack.ml.trainedModels.actions.clearButtonLabel', + { defaultMessage: 'Clear' } +); + +export const CLOSE_BUTTON_LABEL = i18n.translate( + 'xpack.ml.trainedModels.actions.closeButtonLabel', + { defaultMessage: 'Close' } +); + +export const BACK_BUTTON_LABEL = i18n.translate('xpack.ml.trainedModels.actions.backButtonLabel', { + defaultMessage: 'Back', +}); + +export const CONTINUE_BUTTON_LABEL = i18n.translate( + 'xpack.ml.trainedModels.actions.continueButtonLabel', + { defaultMessage: 'Continue' } +); + +export const EDIT_MESSAGE = i18n.translate( + 'xpack.ml.trainedModels.actions.create.advancedDetails.editButtonText', + { + defaultMessage: 'Edit', + } +); + +export const CREATE_FIELD_MAPPING_MESSAGE = i18n.translate( + 'xpack.ml.trainedModels.actions.create.advancedDetails.createFieldMapText', + { + defaultMessage: 'Create field map', + } +); + +export const CANCEL_EDIT_MESSAGE = i18n.translate( + 'xpack.ml.trainedModels.actions.create.advancedDetails.cancelEditButtonText', + { + defaultMessage: 'Cancel', + } +); diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/get_pipeline_config.ts b/x-pack/plugins/ml/public/application/components/ml_inference/get_pipeline_config.ts new file mode 100644 index 0000000000000..d7752a069150e --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/ml_inference/get_pipeline_config.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. + */ + +import type { MlInferenceState } from './types'; + +export function getPipelineConfig(state: MlInferenceState) { + const { + condition, + fieldMap, + ignoreFailure, + inferenceConfig, + modelId, + onFailure, + pipelineDescription, + tag, + targetField, + } = state; + return { + description: pipelineDescription, + processors: [ + { + inference: { + model_id: modelId, + ignore_failure: ignoreFailure, + ...(targetField && targetField !== '' ? { target_field: targetField } : {}), + ...(fieldMap && Object.keys(fieldMap).length > 0 ? { field_map: fieldMap } : {}), + ...(inferenceConfig && Object.keys(inferenceConfig).length > 0 + ? { inference_config: inferenceConfig } + : {}), + ...(condition && condition !== '' ? { if: condition } : {}), + ...(tag && tag !== '' ? { tag } : {}), + ...(onFailure && Object.keys(onFailure).length > 0 ? { on_failure: onFailure } : {}), + }, + }, + ], + }; +} diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/get_steps.ts b/x-pack/plugins/ml/public/application/components/ml_inference/get_steps.ts new file mode 100644 index 0000000000000..a7d3ea17de099 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/ml_inference/get_steps.ts @@ -0,0 +1,47 @@ +/* + * 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 { AddInferencePipelineSteps } from './types'; +import { ADD_INFERENCE_PIPELINE_STEPS } from './constants'; + +export function getSteps( + step: AddInferencePipelineSteps, + isConfigureStepValid: boolean, + isPipelineDataValid: boolean +) { + let nextStep: AddInferencePipelineSteps | undefined; + let previousStep: AddInferencePipelineSteps | undefined; + let isContinueButtonEnabled = false; + + switch (step) { + case ADD_INFERENCE_PIPELINE_STEPS.DETAILS: + nextStep = ADD_INFERENCE_PIPELINE_STEPS.CONFIGURE_PROCESSOR; + isContinueButtonEnabled = isConfigureStepValid; + break; + case ADD_INFERENCE_PIPELINE_STEPS.CONFIGURE_PROCESSOR: + nextStep = ADD_INFERENCE_PIPELINE_STEPS.ON_FAILURE; + previousStep = ADD_INFERENCE_PIPELINE_STEPS.DETAILS; + isContinueButtonEnabled = isPipelineDataValid; + break; + case ADD_INFERENCE_PIPELINE_STEPS.ON_FAILURE: + nextStep = ADD_INFERENCE_PIPELINE_STEPS.TEST; + previousStep = ADD_INFERENCE_PIPELINE_STEPS.CONFIGURE_PROCESSOR; + isContinueButtonEnabled = isPipelineDataValid; + break; + case ADD_INFERENCE_PIPELINE_STEPS.TEST: + nextStep = ADD_INFERENCE_PIPELINE_STEPS.CREATE; + previousStep = ADD_INFERENCE_PIPELINE_STEPS.ON_FAILURE; + isContinueButtonEnabled = true; + break; + case ADD_INFERENCE_PIPELINE_STEPS.CREATE: + previousStep = ADD_INFERENCE_PIPELINE_STEPS.TEST; + isContinueButtonEnabled = true; + break; + } + + return { nextStep, previousStep, isContinueButtonEnabled }; +} diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/hooks/use_fetch_pipelines.ts b/x-pack/plugins/ml/public/application/components/ml_inference/hooks/use_fetch_pipelines.ts new file mode 100644 index 0000000000000..837aef2f92093 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/ml_inference/hooks/use_fetch_pipelines.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { useMlApiContext, useMlKibana } from '../../../contexts/kibana'; + +export const useFetchPipelines = () => { + const [pipelineNames, setPipelineNames] = useState([]); + const { + notifications: { toasts }, + } = useMlKibana(); + + const { + trainedModels: { getAllIngestPipelines }, + } = useMlApiContext(); + + useEffect(() => { + async function fetchPipelines() { + let names: string[] = []; + try { + const results = await getAllIngestPipelines(); + names = Object.keys(results); + setPipelineNames(names); + } catch (e) { + toasts.danger({ + title: i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.fetchIngestPipelinesError', + { + defaultMessage: 'Unable to fetch ingest pipelines.', + } + ), + body: e.message, + toastLifeTimeMs: 5000, + }); + } + } + + fetchPipelines(); + }, [getAllIngestPipelines, toasts]); + + return pipelineNames; +}; diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/index.ts b/x-pack/plugins/ml/public/application/components/ml_inference/index.ts new file mode 100644 index 0000000000000..0c079b0273e98 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/ml_inference/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 { AddInferencePipelineFlyout } from './add_inference_pipeline_flyout'; diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/state.ts b/x-pack/plugins/ml/public/application/components/ml_inference/state.ts new file mode 100644 index 0000000000000..6c74e279fa147 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/ml_inference/state.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getAnalysisType } from '@kbn/ml-data-frame-analytics-utils'; +import type { MlInferenceState } from './types'; +import { ModelItem } from '../../model_management/models_list'; + +export const getModelType = (model: ModelItem): string | undefined => { + const analysisConfig = model.metadata?.analytics_config?.analysis; + return analysisConfig !== undefined ? getAnalysisType(analysisConfig) : undefined; +}; + +export const getDefaultOnFailureConfiguration = (): MlInferenceState['onFailure'] => [ + { + set: { + description: "Index document to 'failed-'", + field: '_index', + value: 'failed-{{{ _index }}}', + }, + }, + { + set: { + field: 'event.timestamp', + value: '{{{ _ingest.timestamp }}}', + }, + }, + { + set: { + field: 'event.failure.message', + value: '{{{ _ingest.on_failure_message }}}', + }, + }, + { + set: { + field: 'event.failure.processor_type', + value: '{{{ _ingest.on_failure_processor_type }}}', + }, + }, + { + set: { + field: 'event.failure.processor_tag', + value: '{{{ _ingest.on_failure_processor_tag }}}', + }, + }, + { + set: { + field: 'event.failure.pipeline', + value: '{{{ _ingest.on_failure_pipeline }}}', + }, + }, +]; + +export const getInitialState = (model: ModelItem): MlInferenceState => { + const modelType = getModelType(model); + let targetField; + + if (modelType !== undefined) { + targetField = model.inference_config + ? `ml.inference.${model.inference_config[modelType].results_field}` + : undefined; + } + + return { + condition: undefined, + creatingPipeline: false, + error: false, + fieldMap: undefined, + ignoreFailure: false, + inferenceConfig: model.inference_config, + modelId: model.model_id, + onFailure: getDefaultOnFailureConfiguration(), + pipelineDescription: `Uses the pre-trained data frame analytics model ${model.model_id} to infer against the data that is being ingested in the pipeline`, + pipelineName: `ml-inference-${model.model_id}`, + pipelineCreated: false, + tag: undefined, + takeActionOnFailure: true, + targetField: targetField ?? '', + }; +}; diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/types.ts b/x-pack/plugins/ml/public/application/components/ml_inference/types.ts new file mode 100644 index 0000000000000..da0aef1a42154 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/ml_inference/types.ts @@ -0,0 +1,48 @@ +/* + * 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 { IngestInferenceProcessor } from '@elastic/elasticsearch/lib/api/types'; +import { ADD_INFERENCE_PIPELINE_STEPS } from './constants'; + +export type AddInferencePipelineSteps = + typeof ADD_INFERENCE_PIPELINE_STEPS[keyof typeof ADD_INFERENCE_PIPELINE_STEPS]; + +export interface MlInferenceState { + condition?: string; + creatingPipeline: boolean; + error: boolean; + fieldMap?: IngestInferenceProcessor['field_map']; + fieldMapError?: string; + ignoreFailure: boolean; + inferenceConfig: IngestInferenceProcessor['inference_config']; + inferenceConfigError?: string; + modelId: string; + onFailure?: IngestInferenceProcessor['on_failure']; + pipelineName: string; + pipelineNameError?: string; + pipelineDescription: string; + pipelineCreated: boolean; + pipelineError?: string; + tag?: string; + targetField: string; + targetFieldError?: string; + takeActionOnFailure: boolean; +} + +export interface AddInferencePipelineFormErrors { + targetField?: string; + fieldMap?: string; + inferenceConfig?: string; + pipelineName?: string; +} + +export type InferenceModelTypes = 'regression' | 'classification'; + +export interface AdditionalSettings { + condition?: string; + tag?: string; +} diff --git a/x-pack/plugins/ml/public/application/components/ml_inference/validation.ts b/x-pack/plugins/ml/public/application/components/ml_inference/validation.ts new file mode 100644 index 0000000000000..c86389607d54a --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/ml_inference/validation.ts @@ -0,0 +1,118 @@ +/* + * 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'; +import { IngestInferenceProcessor } from '@elastic/elasticsearch/lib/api/types'; +import { InferenceModelTypes } from './types'; +import type { AddInferencePipelineFormErrors } from './types'; + +const INVALID_PIPELINE_NAME_ERROR = i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.invalidPipelineName', + { + defaultMessage: 'Name must only contain letters, numbers, underscores, and hyphens.', + } +); +const FIELD_REQUIRED_ERROR = i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.emptyValueError', + { + defaultMessage: 'Field is required.', + } +); +const NO_EMPTY_INFERENCE_CONFIG_OBJECT = i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.noEmptyInferenceConfigObjectError', + { + defaultMessage: 'Inference configuration cannot be an empty object.', + } +); +const PIPELINE_NAME_EXISTS_ERROR = i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.configure.pipelineNameExistsError', + { + defaultMessage: 'Name already used by another pipeline.', + } +); +const FIELD_MAP_REQUIRED_FIELDS_ERROR = i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.emptyValueError', + { + defaultMessage: 'Field map must include fields expected by the model.', + } +); +const INFERENCE_CONFIG_MODEL_TYPE_ERROR = i18n.translate( + 'xpack.ml.trainedModels.content.indices.pipelines.addInferencePipelineModal.steps.advanced.incorrectModelTypeError', + { + defaultMessage: 'Inference configuration inference type must match model type.', + } +); + +const VALID_PIPELINE_NAME_REGEX = /^[\w\-]+$/; +export const isValidPipelineName = (input: string): boolean => { + return input.length > 0 && VALID_PIPELINE_NAME_REGEX.test(input); +}; + +export const validateInferencePipelineConfigurationStep = ( + pipelineName: string, + pipelineNames: string[] +) => { + const errors: AddInferencePipelineFormErrors = {}; + + if (pipelineName.trim().length === 0 || pipelineName === '') { + errors.pipelineName = FIELD_REQUIRED_ERROR; + } else if (!isValidPipelineName(pipelineName)) { + errors.pipelineName = INVALID_PIPELINE_NAME_ERROR; + } + + const pipelineNameExists = pipelineNames.find((name) => name === pipelineName) !== undefined; + + if (pipelineNameExists) { + errors.pipelineName = PIPELINE_NAME_EXISTS_ERROR; + } + + return errors; +}; + +export const validateInferenceConfig = ( + inferenceConfig: IngestInferenceProcessor['inference_config'], + modelType?: InferenceModelTypes +) => { + const inferenceConfigKeys = Object.keys(inferenceConfig ?? {}); + let error; + + // If inference config has been changed, it cannot be an empty object + if (inferenceConfig && Object.keys(inferenceConfig).length === 0) { + error = NO_EMPTY_INFERENCE_CONFIG_OBJECT; + return error; + } + + // If populated, inference config must have the correct model type + if (inferenceConfig && inferenceConfigKeys.length > 0) { + if (modelType === inferenceConfigKeys[0]) { + return error; + } else { + error = INFERENCE_CONFIG_MODEL_TYPE_ERROR; + } + return error; + } + return error; +}; + +export const validateFieldMap = ( + modelInputFields: string[], + fieldMap: IngestInferenceProcessor['field_map'] +) => { + let error; + const fieldMapValues: string[] = Object.values(fieldMap?.field_map ?? {}); + + // If populated, field map must include at least some model input fields as values. + if (fieldMap && fieldMapValues.length > 0) { + if (fieldMapValues.some((v) => modelInputFields.includes(v))) { + return error; + } else { + error = FIELD_MAP_REQUIRED_FIELDS_ERROR; + } + } + + return error; +}; diff --git a/x-pack/plugins/ml/public/application/model_management/model_actions.tsx b/x-pack/plugins/ml/public/application/model_management/model_actions.tsx index 09ba11f56c747..9cd0794d1b488 100644 --- a/x-pack/plugins/ml/public/application/model_management/model_actions.tsx +++ b/x-pack/plugins/ml/public/application/model_management/model_actions.tsx @@ -35,6 +35,7 @@ import { ModelItem } from './models_list'; export function useModelActions({ onTestAction, onModelsDeleteRequest, + onModelDeployRequest, onLoading, isLoading, fetchModels, @@ -43,6 +44,7 @@ export function useModelActions({ isLoading: boolean; onTestAction: (model: ModelItem) => void; onModelsDeleteRequest: (models: ModelItem[]) => void; + onModelDeployRequest: (model: ModelItem) => void; onLoading: (isLoading: boolean) => void; fetchModels: () => Promise; modelAndDeploymentIds: string[]; @@ -412,6 +414,54 @@ export function useModelActions({ } }, }, + { + name: (model) => { + const hasDeployments = model.state === MODEL_STATE.STARTED; + return ( + + <> + {i18n.translate('xpack.ml.trainedModels.modelsList.deployModelActionLabel', { + defaultMessage: 'Deploy model', + })} + + + ); + }, + description: i18n.translate('xpack.ml.trainedModels.modelsList.deployModelActionLabel', { + defaultMessage: 'Deploy model', + }), + 'data-test-subj': 'mlModelsTableRowDeployAction', + icon: 'continuityAbove', + type: 'icon', + isPrimary: false, + onClick: (model) => { + onModelDeployRequest(model); + }, + available: (item) => { + const isDfaTrainedModel = item.metadata?.analytics_config !== undefined; + return ( + isDfaTrainedModel && + !isBuiltInModel(item) && + !item.putModelConfig && + canManageIngestPipelines + ); + }, + enabled: (item) => { + return item.state !== MODEL_STATE.STARTED; + }, + }, { name: (model) => { const hasDeployments = model.state === MODEL_STATE.STARTED; @@ -492,6 +542,7 @@ export function useModelActions({ displayErrorToast, getUserConfirmation, onModelsDeleteRequest, + onModelDeployRequest, canDeleteTrainedModels, isBuiltInModel, onTestAction, diff --git a/x-pack/plugins/ml/public/application/model_management/models_list.tsx b/x-pack/plugins/ml/public/application/model_management/models_list.tsx index d5f15c10aa2c6..653963155bc68 100644 --- a/x-pack/plugins/ml/public/application/model_management/models_list.tsx +++ b/x-pack/plugins/ml/public/application/model_management/models_list.tsx @@ -62,6 +62,7 @@ import { useFieldFormatter } from '../contexts/kibana/use_field_formatter'; import { useRefresh } from '../routing/use_refresh'; import { SavedObjectsWarning } from '../components/saved_objects_warning'; import { TestTrainedModelFlyout } from './test_models'; +import { AddInferencePipelineFlyout } from '../components/ml_inference'; type Stats = Omit; @@ -134,6 +135,7 @@ export const ModelsList: FC = ({ const [items, setItems] = useState([]); const [selectedModels, setSelectedModels] = useState([]); const [modelsToDelete, setModelsToDelete] = useState([]); + const [modelToDeploy, setModelToDeploy] = useState(); const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState>( {} ); @@ -349,6 +351,7 @@ export const ModelsList: FC = ({ fetchModels: fetchModelsData, onTestAction: setModelToTest, onModelsDeleteRequest: setModelsToDelete, + onModelDeployRequest: setModelToDeploy, onLoading: setIsLoading, modelAndDeploymentIds, }); @@ -642,6 +645,12 @@ export const ModelsList: FC = ({ {modelToTest === null ? null : ( )} + {modelToDeploy !== undefined ? ( + + ) : null} ); }; diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/trained_models.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/trained_models.ts index 0ea4b1d1fde4b..e6b9c1a5badc3 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/trained_models.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/trained_models.ts @@ -6,6 +6,7 @@ */ import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { IngestPipeline } from '@elastic/elasticsearch/lib/api/types'; import { useMemo } from 'react'; import type { HttpFetchQuery } from '@kbn/core/public'; @@ -58,7 +59,6 @@ export function trainedModelsApiProvider(httpService: HttpService) { return { /** * Fetches configuration information for a trained inference model. - * * @param modelId - Model ID, collection of Model IDs or Model ID pattern. * Fetches all In case nothing is provided. * @param params - Optional query params @@ -76,7 +76,6 @@ export function trainedModelsApiProvider(httpService: HttpService) { /** * Fetches usage information for trained inference models. - * * @param modelId - Model ID, collection of Model IDs or Model ID pattern. * Fetches all In case nothing is provided. * @param params - Optional query params @@ -93,7 +92,6 @@ export function trainedModelsApiProvider(httpService: HttpService) { /** * Fetches pipelines associated with provided models - * * @param modelId - Model ID, collection of Model IDs. */ getTrainedModelPipelines(modelId: string | string[]) { @@ -109,9 +107,31 @@ export function trainedModelsApiProvider(httpService: HttpService) { }); }, + /** + * Fetches all ingest pipelines + */ + getAllIngestPipelines() { + return httpService.http({ + path: `${ML_INTERNAL_BASE_PATH}/trained_models/ingest_pipelines`, + method: 'GET', + version: '1', + }); + }, + + /** + * Creates inference pipeline + */ + createInferencePipeline(pipelineName: string, pipeline: IngestPipeline) { + return httpService.http({ + path: `${ML_INTERNAL_BASE_PATH}/trained_models/create_inference_pipeline`, + method: 'POST', + body: JSON.stringify({ pipeline, pipelineName }), + version: '1', + }); + }, + /** * Deletes an existing trained inference model. - * * @param modelId - Model ID */ deleteTrainedModel( diff --git a/x-pack/plugins/ml/scripts/apidoc_scripts/apidoc_config/apidoc.json b/x-pack/plugins/ml/scripts/apidoc_scripts/apidoc_config/apidoc.json index 978dbf9dc94f3..90dbdbd9c121f 100644 --- a/x-pack/plugins/ml/scripts/apidoc_scripts/apidoc_config/apidoc.json +++ b/x-pack/plugins/ml/scripts/apidoc_scripts/apidoc_config/apidoc.json @@ -177,6 +177,8 @@ "DeleteTrainedModel", "SimulateIngestPipeline", "InferTrainedModelDeployment", + "CreateInferencePipeline", + "GetIngestPipelines", "Alerting", "PreviewAlert", diff --git a/x-pack/plugins/ml/server/models/model_management/models_provider.ts b/x-pack/plugins/ml/server/models/model_management/models_provider.ts index 702e8454660f6..e7cfcbe7fd50d 100644 --- a/x-pack/plugins/ml/server/models/model_management/models_provider.ts +++ b/x-pack/plugins/ml/server/models/model_management/models_provider.ts @@ -6,6 +6,11 @@ */ import type { IScopedClusterClient } from '@kbn/core/server'; +import { + IngestPipeline, + IngestSimulateDocument, + IngestSimulateRequest, +} from '@elastic/elasticsearch/lib/api/types'; import type { PipelineDefinition } from '../../../common/types/trained_models'; export type ModelService = ReturnType; @@ -64,5 +69,64 @@ export function modelsProvider(client: IScopedClusterClient) { pipelinesIds.map((id) => client.asCurrentUser.ingest.deletePipeline({ id })) ); }, + + /** + * Simulates the effect of the pipeline on given document. + * + */ + async simulatePipeline(docs: IngestSimulateDocument[], pipelineConfig: IngestPipeline) { + const simulateRequest: IngestSimulateRequest = { + docs, + pipeline: pipelineConfig, + }; + let result = {}; + try { + result = await client.asCurrentUser.ingest.simulate(simulateRequest); + } catch (error) { + if (error.statusCode === 404) { + // ES returns 404 when there are no pipelines + // Instead, we should return an empty response and a 200 + return result; + } + throw error; + } + + return result; + }, + + /** + * Creates the pipeline + * + */ + async createInferencePipeline(pipelineConfig: IngestPipeline, pipelineName: string) { + let result = {}; + + result = await client.asCurrentUser.ingest.putPipeline({ + id: pipelineName, + ...pipelineConfig, + }); + + return result; + }, + + /** + * Retrieves existing pipelines. + * + */ + async getPipelines() { + let result = {}; + try { + result = await client.asCurrentUser.ingest.getPipeline(); + } catch (error) { + if (error.statusCode === 404) { + // ES returns 404 when there are no pipelines + // Instead, we should return an empty response and a 200 + return result; + } + throw error; + } + + return result; + }, }; } diff --git a/x-pack/plugins/ml/server/routes/schemas/inference_schema.ts b/x-pack/plugins/ml/server/routes/schemas/inference_schema.ts index e15fd108c5f5b..21b62c6f5ce42 100644 --- a/x-pack/plugins/ml/server/routes/schemas/inference_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/inference_schema.ts @@ -77,3 +77,13 @@ export const deleteTrainedModelQuerySchema = schema.object({ with_pipelines: schema.maybe(schema.boolean({ defaultValue: false })), force: schema.maybe(schema.boolean({ defaultValue: false })), }); + +export const createIngestPipelineSchema = schema.object({ + pipelineName: schema.string(), + pipeline: schema.maybe( + schema.object({ + processors: schema.arrayOf(schema.any()), + description: schema.maybe(schema.string()), + }) + ), +}); diff --git a/x-pack/plugins/ml/server/routes/trained_models.ts b/x-pack/plugins/ml/server/routes/trained_models.ts index cefb8a9dce0fd..c85429f41a72a 100644 --- a/x-pack/plugins/ml/server/routes/trained_models.ts +++ b/x-pack/plugins/ml/server/routes/trained_models.ts @@ -22,6 +22,7 @@ import { putTrainedModelQuerySchema, threadingParamsSchema, updateDeploymentParamsSchema, + createIngestPipelineSchema, } from './schemas/inference_schema'; import { TrainedModelConfigResponse } from '../../common/types/trained_models'; import { mlLog } from '../lib/log'; @@ -237,6 +238,78 @@ export function trainedModelsRoutes({ router, routeGuard }: RouteInitialization) }) ); + /** + * @apiGroup TrainedModels + * + * @api {get} /internal/ml/trained_models/ingest_pipelines Get ingest pipelines + * @apiName GetIngestPipelines + * @apiDescription Retrieves pipelines + */ + router.versioned + .get({ + path: `${ML_INTERNAL_BASE_PATH}/trained_models/ingest_pipelines`, + access: 'internal', + options: { + tags: ['access:ml:canGetTrainedModels'], // TODO: update permissions + }, + }) + .addVersion( + { + version: '1', + validate: false, + }, + routeGuard.fullLicenseAPIGuard(async ({ client, request, mlClient, response }) => { + try { + const body = await modelsProvider(client).getPipelines(); + return response.ok({ + body, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + + /** + * @apiGroup TrainedModels + * + * @api {post} /internal/ml/trained_models/create_inference_pipeline creates the pipeline with inference processor + * @apiName CreateInferencePipeline + * @apiDescription Creates the inference pipeline + */ + router.versioned + .post({ + path: `${ML_INTERNAL_BASE_PATH}/trained_models/create_inference_pipeline`, + access: 'internal', + options: { + tags: ['access:ml:canCreateTrainedModels'], + }, + }) + .addVersion( + { + version: '1', + validate: { + request: { + body: createIngestPipelineSchema, + }, + }, + }, + routeGuard.fullLicenseAPIGuard(async ({ client, request, mlClient, response }) => { + try { + const { pipeline, pipelineName } = request.body; + const body = await modelsProvider(client).createInferencePipeline( + pipeline!, + pipelineName + ); + return response.ok({ + body, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); + /** * @apiGroup TrainedModels * diff --git a/x-pack/plugins/rollup/kibana.jsonc b/x-pack/plugins/rollup/kibana.jsonc index 7bb5740ff8b2b..73f0e76b16d73 100644 --- a/x-pack/plugins/rollup/kibana.jsonc +++ b/x-pack/plugins/rollup/kibana.jsonc @@ -13,13 +13,14 @@ "requiredPlugins": [ "management", "licensing", - "features" + "features", + "dataViews", + "data" ], "optionalPlugins": [ "home", "indexManagement", - "usageCollection", - "visTypeTimeseries" + "usageCollection" ], "requiredBundles": [ "kibanaUtils", diff --git a/x-pack/plugins/rollup/server/plugin.ts b/x-pack/plugins/rollup/server/plugin.ts index 06416685ab508..409c730385db9 100644 --- a/x-pack/plugins/rollup/server/plugin.ts +++ b/x-pack/plugins/rollup/server/plugin.ts @@ -30,8 +30,8 @@ export class RollupPlugin implements Plugin { } public setup( - { http, uiSettings, savedObjects, getStartServices }: CoreSetup, - { features, licensing, indexManagement, visTypeTimeseries, usageCollection }: Dependencies + { http, uiSettings, getStartServices }: CoreSetup, + { features, licensing, indexManagement, usageCollection, dataViews, data }: Dependencies ) { this.license.setup( { @@ -103,6 +103,8 @@ export class RollupPlugin implements Plugin { if (indexManagement && indexManagement.indexDataEnricher) { indexManagement.indexDataEnricher.add(rollupDataEnricher); } + dataViews.enableRollups(); + data.search.enableRollups(); } start() {} diff --git a/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts b/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts index 99312936adba4..c32e0e2be8f5f 100644 --- a/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts +++ b/x-pack/plugins/rollup/server/routes/api/indices/register_get_route.ts @@ -18,6 +18,7 @@ export const registerGetRoute = ({ }: RouteDependencies) => { router.get( { + // this endpoint is used by the data views plugin, see https://github.com/elastic/kibana/issues/152708 path: addBasePath('/indices'), validate: false, }, diff --git a/x-pack/plugins/rollup/server/types.ts b/x-pack/plugins/rollup/server/types.ts index 177efe0915fcf..29d2fe2e99771 100644 --- a/x-pack/plugins/rollup/server/types.ts +++ b/x-pack/plugins/rollup/server/types.ts @@ -12,6 +12,8 @@ import { VisTypeTimeseriesSetup } from '@kbn/vis-type-timeseries-plugin/server'; import { getCapabilitiesForRollupIndices } from '@kbn/data-plugin/server'; import { IndexManagementPluginSetup } from '@kbn/index-management-plugin/server'; import { PluginSetupContract as FeaturesPluginSetup } from '@kbn/features-plugin/server'; +import { DataViewsServerPluginSetup } from '@kbn/data-views-plugin/server'; +import { PluginSetup as DataPluginSetup } from '@kbn/data-plugin/server'; import { LicensingPluginSetup } from '@kbn/licensing-plugin/server'; import { License } from './services'; import { IndexPatternsFetcher } from './shared_imports'; @@ -24,6 +26,8 @@ export interface Dependencies { usageCollection?: UsageCollectionSetup; licensing: LicensingPluginSetup; features: FeaturesPluginSetup; + dataViews: DataViewsServerPluginSetup; + data: DataPluginSetup; } export interface RouteDependencies { diff --git a/x-pack/plugins/rollup/tsconfig.json b/x-pack/plugins/rollup/tsconfig.json index 151c5151a0c17..366b44b2c33be 100644 --- a/x-pack/plugins/rollup/tsconfig.json +++ b/x-pack/plugins/rollup/tsconfig.json @@ -32,6 +32,7 @@ "@kbn/i18n-react", "@kbn/config-schema", "@kbn/shared-ux-router", + "@kbn/data-views-plugin", ], "exclude": [ diff --git a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_prevalence_tab.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_prevalence_tab.cy.ts index 599243ae37d29..3cfe58f22893c 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_prevalence_tab.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/investigations/alerts/expandable_flyout/alert_details_left_panel_prevalence_tab.cy.ts @@ -70,7 +70,7 @@ describe( ); cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_DOC_COUNT_CELL).should( 'contain.text', - 2 + 0 ); cy.get(DOCUMENT_DETAILS_FLYOUT_INSIGHTS_TAB_PREVALENCE_TABLE_HOST_PREVALENCE_CELL).should( 'contain.text', diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.tsx index b5984d66a5f56..70fc7554e64cd 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details.tsx @@ -37,7 +37,6 @@ import { EventKind } from '../../shared/hooks/use_fetch_field_value_pair_by_even interface PrevalenceDetailsTableCell { highlightedField: { name: string; values: string[] }; - scopeId: string; } export const PREVALENCE_TAB_ID = 'prevalence-details'; @@ -60,7 +59,6 @@ const columns: Array> = [ render: (data: PrevalenceDetailsTableCell) => ( > = [ render: (data: PrevalenceDetailsTableCell) => ( > = [ render: (data: PrevalenceDetailsTableCell) => ( ), @@ -102,7 +98,6 @@ const columns: Array> = [ render: (data: PrevalenceDetailsTableCell) => ( ), @@ -129,7 +124,6 @@ export const PrevalenceDetails: React.FC = () => { name: summaryRow.description.data.field, values: summaryRow.description.values || [], }, - scopeId, }); return (summaryRows || []).map((summaryRow) => { diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details_count_cell.test.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details_count_cell.test.tsx index 31f56ada7b5f2..5b81565ec2804 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details_count_cell.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details_count_cell.test.tsx @@ -24,7 +24,6 @@ const highlightedField = { name: 'field', values: ['values'], }; -const scopeId = 'scopeId'; const type = { eventKind: EventKind.signal, include: true, @@ -39,11 +38,7 @@ describe('PrevalenceDetailsAlertCountCell', () => { }); const { getByTestId } = render( - + ); expect(getByTestId(PREVALENCE_DETAILS_COUNT_CELL_LOADING_TEST_ID)).toBeInTheDocument(); @@ -57,11 +52,7 @@ describe('PrevalenceDetailsAlertCountCell', () => { }); const { getByTestId } = render( - + ); expect(getByTestId(PREVALENCE_DETAILS_COUNT_CELL_ERROR_TEST_ID)).toBeInTheDocument(); @@ -75,11 +66,7 @@ describe('PrevalenceDetailsAlertCountCell', () => { }); const { getByTestId } = render( - + ); expect(getByTestId(PREVALENCE_DETAILS_COUNT_CELL_VALUE_TEST_ID)).toBeInTheDocument(); @@ -94,11 +81,7 @@ describe('PrevalenceDetailsAlertCountCell', () => { }); const { getByTestId } = render( - + ); expect(getByTestId(PREVALENCE_DETAILS_COUNT_CELL_VALUE_TEST_ID)).toBeInTheDocument(); diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details_count_cell.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details_count_cell.tsx index 72c110de353c6..a2e3959a9b1c6 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details_count_cell.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details_count_cell.tsx @@ -15,17 +15,12 @@ import { } from './test_ids'; import type { EventType } from '../../shared/hooks/use_fetch_field_value_pair_by_event_type'; import { useFetchFieldValuePairByEventType } from '../../shared/hooks/use_fetch_field_value_pair_by_event_type'; -import { TimelineId } from '../../../../common/types'; export interface PrevalenceDetailsCountCellProps { /** * The highlighted field name and values * */ highlightedField: { name: string; values: string[] }; - /** - * The scope id - */ - scopeId: string; /** * Limit the search to include or exclude a specific value for the event.kind field * (alert, asset, enrichment, event, metric, state, pipeline_error, signal) @@ -41,12 +36,10 @@ export interface PrevalenceDetailsCountCellProps { */ export const PrevalenceDetailsCountCell: VFC = ({ highlightedField, - scopeId, type, }) => { const { loading, error, count } = useFetchFieldValuePairByEventType({ highlightedField, - isActiveTimelines: scopeId === TimelineId.active, type, }); diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details_prevalence_cell.test.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details_prevalence_cell.test.tsx index 3837162672bfb..b4bf809665f98 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details_prevalence_cell.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details_prevalence_cell.test.tsx @@ -23,7 +23,6 @@ const highlightedField = { name: 'field', values: ['values'], }; -const scopeId = 'scopeId'; const aggregationField = 'aggregationField'; describe('PrevalenceDetailsAlertCountCell', () => { @@ -42,7 +41,6 @@ describe('PrevalenceDetailsAlertCountCell', () => { const { getByTestId } = render( ); @@ -65,7 +63,6 @@ describe('PrevalenceDetailsAlertCountCell', () => { const { getByTestId } = render( ); @@ -88,7 +85,6 @@ describe('PrevalenceDetailsAlertCountCell', () => { const { getByTestId } = render( ); @@ -111,7 +107,6 @@ describe('PrevalenceDetailsAlertCountCell', () => { const { getByTestId } = render( ); @@ -134,7 +129,6 @@ describe('PrevalenceDetailsAlertCountCell', () => { const { getByTestId } = render( ); @@ -157,7 +151,6 @@ describe('PrevalenceDetailsAlertCountCell', () => { const { getByTestId } = render( ); diff --git a/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details_prevalence_cell.tsx b/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details_prevalence_cell.tsx index ad8fa2e49289c..9c05bc8e8ea32 100644 --- a/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details_prevalence_cell.tsx +++ b/x-pack/plugins/security_solution/public/flyout/left/components/prevalence_details_prevalence_cell.tsx @@ -14,7 +14,6 @@ import { PREVALENCE_DETAILS_PREVALENCE_CELL_VALUE_TEST_ID, } from './test_ids'; import { useFetchFieldValuePairWithAggregation } from '../../shared/hooks/use_fetch_field_value_pair_with_aggregation'; -import { TimelineId } from '../../../../common/types'; import { useFetchUniqueByField } from '../../shared/hooks/use_fetch_unique_by_field'; export interface PrevalenceDetailsPrevalenceCellProps { @@ -22,10 +21,6 @@ export interface PrevalenceDetailsPrevalenceCellProps { * The highlighted field name and values * */ highlightedField: { name: string; values: string[] }; - /** - * The scope id - */ - scopeId: string; /** * The aggregation field */ @@ -38,7 +33,6 @@ export interface PrevalenceDetailsPrevalenceCellProps { */ export const PrevalenceDetailsPrevalenceCell: VFC = ({ highlightedField, - scopeId, aggregationField, }) => { const { @@ -47,7 +41,6 @@ export const PrevalenceDetailsPrevalenceCell: VFC', () => { prevalenceRows: [ , @@ -106,7 +104,6 @@ describe('', () => { prevalenceRows: [ , diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview_row.test.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview_row.test.tsx index ac6a76e606444..fc1a4ce2b1a3f 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview_row.test.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview_row.test.tsx @@ -18,7 +18,6 @@ const highlightedField = { name: 'field', values: ['values'], }; -const scopeId = 'scopeId'; const dataTestSubj = 'test'; const iconDataTestSubj = 'testIcon'; const valueDataTestSubj = 'testValue'; @@ -41,7 +40,6 @@ describe('', () => { const { getByTestId, getAllByText, queryByTestId } = render( {}} data-test-subj={dataTestSubj} /> @@ -71,7 +69,6 @@ describe('', () => { const { queryAllByAltText } = render( @@ -97,7 +94,6 @@ describe('', () => { const { queryAllByAltText } = render( @@ -122,7 +118,6 @@ describe('', () => { const { getByTestId } = render( {}} data-test-subj={dataTestSubj} /> diff --git a/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview_row.tsx b/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview_row.tsx index 0fd37af5929e8..d7cef2fe99f8b 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview_row.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/components/prevalence_overview_row.tsx @@ -11,7 +11,6 @@ import { PREVALENCE_ROW_UNCOMMON } from './translations'; import { useFetchFieldValuePairWithAggregation } from '../../shared/hooks/use_fetch_field_value_pair_with_aggregation'; import { useFetchUniqueByField } from '../../shared/hooks/use_fetch_unique_by_field'; import { InsightsSummaryRow } from './insights_summary_row'; -import { TimelineId } from '../../../../common/types'; const HOST_FIELD = 'host.name'; const PERCENTAGE_THRESHOLD = 0.1; // we show the prevalence if its value is below 10% @@ -21,10 +20,6 @@ export interface PrevalenceOverviewRowProps { * The highlighted field name and values * */ highlightedField: { name: string; values: string[] }; - /** - * Maintain backwards compatibility // TODO remove when possible - */ - scopeId: string; /** * This is a solution to allow the parent component to NOT render if all its row children are null */ @@ -42,19 +37,15 @@ export interface PrevalenceOverviewRowProps { */ export const PrevalenceOverviewRow: VFC = ({ highlightedField, - scopeId, callbackIfNull, 'data-test-subj': dataTestSubj, }) => { - const isActiveTimelines = scopeId === TimelineId.active; - const { loading: hostsLoading, error: hostsError, count: hostsCount, } = useFetchFieldValuePairWithAggregation({ highlightedField, - isActiveTimelines, aggregationField: HOST_FIELD, }); diff --git a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_prevalence.tsx b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_prevalence.tsx index 336d9f251f52e..82a359d0e70f1 100644 --- a/x-pack/plugins/security_solution/public/flyout/right/hooks/use_prevalence.tsx +++ b/x-pack/plugins/security_solution/public/flyout/right/hooks/use_prevalence.tsx @@ -79,14 +79,13 @@ export const usePrevalence = ({ return ( setCount((prevCount) => prevCount + 1)} data-test-subj={INSIGHTS_PREVALENCE_TEST_ID} key={row.description.data.field} /> ); }), - [summaryRows, scopeId] + [summaryRows] ); return { diff --git a/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_fetch_field_value_pair_by_event_type.test.ts b/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_fetch_field_value_pair_by_event_type.test.ts index 8bbd039ff333a..bac2f7b37af63 100644 --- a/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_fetch_field_value_pair_by_event_type.test.ts +++ b/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_fetch_field_value_pair_by_event_type.test.ts @@ -9,8 +9,6 @@ import { useQuery } from '@tanstack/react-query'; import type { RenderHookResult } from '@testing-library/react-hooks'; import { renderHook } from '@testing-library/react-hooks'; import { useKibana } from '../../../common/lib/kibana'; -import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; -import { useGlobalTime } from '../../../common/containers/use_global_time'; import type { UseFetchFieldValuePairByEventTypeParams, UseFetchFieldValuePairByEventTypeResult, @@ -22,14 +20,11 @@ import { jest.mock('@tanstack/react-query'); jest.mock('../../../common/lib/kibana'); -jest.mock('../../../common/hooks/use_selector'); -jest.mock('../../../common/containers/use_global_time'); const highlightedField = { name: 'field', values: ['values'], }; -const isActiveTimelines = true; const type = { eventKind: EventKind.alert, include: true, @@ -45,8 +40,6 @@ describe('useFetchFieldValuePairByEventType', () => { data: { search: jest.fn() }, }, }); - jest.mocked(useDeepEqualSelector).mockReturnValue({ to: '', from: '' }); - (useGlobalTime as jest.Mock).mockReturnValue({ to: '', from: '' }); it('should return loading true while data is being fetched', () => { (useQuery as jest.Mock).mockReturnValue({ @@ -55,9 +48,7 @@ describe('useFetchFieldValuePairByEventType', () => { data: 0, }); - hookResult = renderHook(() => - useFetchFieldValuePairByEventType({ highlightedField, isActiveTimelines, type }) - ); + hookResult = renderHook(() => useFetchFieldValuePairByEventType({ highlightedField, type })); expect(hookResult.result.current.loading).toBeTruthy(); expect(hookResult.result.current.error).toBeFalsy(); @@ -71,9 +62,7 @@ describe('useFetchFieldValuePairByEventType', () => { data: 0, }); - hookResult = renderHook(() => - useFetchFieldValuePairByEventType({ highlightedField, isActiveTimelines, type }) - ); + hookResult = renderHook(() => useFetchFieldValuePairByEventType({ highlightedField, type })); expect(hookResult.result.current.loading).toBeFalsy(); expect(hookResult.result.current.error).toBeTruthy(); @@ -87,9 +76,7 @@ describe('useFetchFieldValuePairByEventType', () => { data: 1, }); - hookResult = renderHook(() => - useFetchFieldValuePairByEventType({ highlightedField, isActiveTimelines, type }) - ); + hookResult = renderHook(() => useFetchFieldValuePairByEventType({ highlightedField, type })); expect(hookResult.result.current.loading).toBeFalsy(); expect(hookResult.result.current.error).toBeFalsy(); diff --git a/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_fetch_field_value_pair_by_event_type.ts b/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_fetch_field_value_pair_by_event_type.ts index 4f8312c5bec0e..c1e52b90558e5 100644 --- a/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_fetch_field_value_pair_by_event_type.ts +++ b/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_fetch_field_value_pair_by_event_type.ts @@ -10,12 +10,11 @@ import type { IEsSearchRequest } from '@kbn/data-plugin/public'; import { useQuery } from '@tanstack/react-query'; import { createFetchData } from '../utils/fetch_data'; import { useKibana } from '../../../common/lib/kibana'; -import { inputsSelectors } from '../../../common/store'; -import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; -import { useGlobalTime } from '../../../common/containers/use_global_time'; import type { RawResponse } from '../utils/fetch_data'; const QUERY_KEY = 'FetchFieldValuePairByEventType'; +const DEFAULT_FROM = 'now-30d'; +const DEFAULT_TO = 'now'; export enum EventKind { alert = 'alert', @@ -39,10 +38,6 @@ export interface UseFetchFieldValuePairByEventTypeParams { * The highlighted field name and values * */ highlightedField: { name: string; values: string[] }; - /** - * True is the current timeline is active ('timeline-1') - */ - isActiveTimelines: boolean; /** * Limit the search to include or exclude a specific value for the event.kind field * (alert, asset, enrichment, event, metric, state, pipeline_error, signal) @@ -70,7 +65,6 @@ export interface UseFetchFieldValuePairByEventTypeResult { */ export const useFetchFieldValuePairByEventType = ({ highlightedField, - isActiveTimelines, type, }: UseFetchFieldValuePairByEventTypeParams): UseFetchFieldValuePairByEventTypeResult => { const { @@ -79,11 +73,7 @@ export const useFetchFieldValuePairByEventType = ({ }, } = useKibana(); - const timelineTime = useDeepEqualSelector((state) => - inputsSelectors.timelineTimeRangeSelector(state) - ); - const globalTime = useGlobalTime(); - const { to, from } = isActiveTimelines ? timelineTime : globalTime; + const { from, to } = { from: DEFAULT_FROM, to: DEFAULT_TO }; const { name, values } = highlightedField; diff --git a/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_fetch_field_value_pair_with_aggregation.test.ts b/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_fetch_field_value_pair_with_aggregation.test.ts index a1ec575db1244..7de92d29cc17e 100644 --- a/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_fetch_field_value_pair_with_aggregation.test.ts +++ b/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_fetch_field_value_pair_with_aggregation.test.ts @@ -14,19 +14,14 @@ import type { UseFetchFieldValuePairWithAggregationResult, } from './use_fetch_field_value_pair_with_aggregation'; import { useFetchFieldValuePairWithAggregation } from './use_fetch_field_value_pair_with_aggregation'; -import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; -import { useGlobalTime } from '../../../common/containers/use_global_time'; jest.mock('@tanstack/react-query'); jest.mock('../../../common/lib/kibana'); -jest.mock('../../../common/hooks/use_selector'); -jest.mock('../../../common/containers/use_global_time'); const highlightedField = { name: 'field', values: ['values'], }; -const isActiveTimelines = true; const aggregationField = 'aggregationField'; describe('useFetchFieldValuePairWithAggregation', () => { @@ -39,8 +34,6 @@ describe('useFetchFieldValuePairWithAggregation', () => { data: { search: jest.fn() }, }, }); - jest.mocked(useDeepEqualSelector).mockReturnValue({ to: '', from: '' }); - (useGlobalTime as jest.Mock).mockReturnValue({ to: '', from: '' }); it('should return loading true while data is being fetched', () => { (useQuery as jest.Mock).mockReturnValue({ @@ -52,7 +45,6 @@ describe('useFetchFieldValuePairWithAggregation', () => { hookResult = renderHook(() => useFetchFieldValuePairWithAggregation({ highlightedField, - isActiveTimelines, aggregationField, }) ); @@ -72,7 +64,6 @@ describe('useFetchFieldValuePairWithAggregation', () => { hookResult = renderHook(() => useFetchFieldValuePairWithAggregation({ highlightedField, - isActiveTimelines, aggregationField, }) ); @@ -92,7 +83,6 @@ describe('useFetchFieldValuePairWithAggregation', () => { hookResult = renderHook(() => useFetchFieldValuePairWithAggregation({ highlightedField, - isActiveTimelines, aggregationField, }) ); diff --git a/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_fetch_field_value_pair_with_aggregation.ts b/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_fetch_field_value_pair_with_aggregation.ts index 86c30e6008b36..16be9931db1f3 100644 --- a/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_fetch_field_value_pair_with_aggregation.ts +++ b/x-pack/plugins/security_solution/public/flyout/shared/hooks/use_fetch_field_value_pair_with_aggregation.ts @@ -12,21 +12,16 @@ import { buildAggregationSearchRequest } from '../utils/build_requests'; import type { RawAggregatedDataResponse } from '../utils/fetch_data'; import { AGG_KEY, createFetchData } from '../utils/fetch_data'; import { useKibana } from '../../../common/lib/kibana'; -import { inputsSelectors } from '../../../common/store'; -import { useDeepEqualSelector } from '../../../common/hooks/use_selector'; -import { useGlobalTime } from '../../../common/containers/use_global_time'; const QUERY_KEY = 'useFetchFieldValuePairWithAggregation'; +const DEFAULT_FROM = 'now-30d'; +const DEFAULT_TO = 'now'; export interface UseFetchFieldValuePairWithAggregationParams { /** * The highlighted field name and values * */ highlightedField: { name: string; values: string[] }; - /** - * - */ - isActiveTimelines: boolean; /** * Field to aggregate value by */ @@ -55,7 +50,6 @@ export interface UseFetchFieldValuePairWithAggregationResult { */ export const useFetchFieldValuePairWithAggregation = ({ highlightedField, - isActiveTimelines, aggregationField, }: UseFetchFieldValuePairWithAggregationParams): UseFetchFieldValuePairWithAggregationResult => { const { @@ -64,12 +58,7 @@ export const useFetchFieldValuePairWithAggregation = ({ }, } = useKibana(); - const timelineTime = useDeepEqualSelector((state) => - inputsSelectors.timelineTimeRangeSelector(state) - ); - const globalTime = useGlobalTime(); - const { to, from } = isActiveTimelines ? timelineTime : globalTime; - + const { from, to } = { from: DEFAULT_FROM, to: DEFAULT_TO }; const { name, values } = highlightedField; const searchRequest = buildSearchRequest(name, values, from, to, aggregationField); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/components/endpoint_event_collection_preset.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/components/endpoint_event_collection_preset.tsx new file mode 100644 index 0000000000000..b07ab081a638f --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/components/endpoint_event_collection_preset.tsx @@ -0,0 +1,107 @@ +/* + * 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, { memo, useEffect, useMemo } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiRadio, EuiSpacer } from '@elastic/eui'; +import type { + NewPackagePolicy, + PackagePolicyCreateExtensionComponentProps, +} from '@kbn/fleet-plugin/public'; +import type { EndpointPreset } from '../constants'; +import { ENDPOINT_INTEGRATION_CONFIG_KEY } from '../constants'; +import { HelpTextWithPadding } from './help_text_with_padding'; +import { DATA_COLLECTION, DATA_COLLECTION_HELP_TEXT } from '../translations'; +import { useGetProtectionsUnavailableComponent } from '../../../policy_settings_form/hooks/use_get_protections_unavailable_component'; + +const NOOP = () => {}; + +type EndpointEventCollectionPresetProps = PackagePolicyCreateExtensionComponentProps; + +/** + * Display ONLY the event collection option on the screen along with the upselling message + */ +export const EndpointEventCollectionPreset = memo( + ({ onChange, newPolicy }) => { + const UpsellToIncludePolicyProtections = useGetProtectionsUnavailableComponent(); + const preset: EndpointPreset = 'DataCollection'; + const policyInputs: NewPackagePolicy['inputs'] = useMemo(() => { + return [ + { + enabled: true, + streams: [], + type: ENDPOINT_INTEGRATION_CONFIG_KEY, + config: { + _config: { + value: { + type: 'endpoint', + endpointConfig: { + preset, + }, + }, + }, + }, + }, + ]; + }, []); + + useEffect(() => { + const inputs = newPolicy.inputs; + + if (inputs.length === 0) { + onChange({ + isValid: false, + updatedPolicy: { + ...newPolicy, + name: '', + inputs: policyInputs, + }, + }); + return; + } + + if (inputs[0]?.config?._config.value.endpointConfig.preset !== preset) { + onChange({ + isValid: true, + updatedPolicy: { + ...newPolicy, + inputs: policyInputs, + }, + }); + } + }, [newPolicy, onChange, policyInputs]); + + return ( +
+ + {DATA_COLLECTION_HELP_TEXT}} + > + + + + + {DATA_COLLECTION} + + + + {UpsellToIncludePolicyProtections && ( + <> + + + + )} +
+ ); + } +); +EndpointEventCollectionPreset.displayName = 'EndpointEventCollectionPreset'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/components/help_text_with_padding.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/components/help_text_with_padding.tsx new file mode 100644 index 0000000000000..3fd4e8d60dbca --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/components/help_text_with_padding.tsx @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import styled from 'styled-components'; + +export const HelpTextWithPadding = styled.div` + padding-left: ${(props) => props.theme.eui.euiSizeL}; +`; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/constants.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/constants.ts new file mode 100644 index 0000000000000..c5606bb41a05d --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/constants.ts @@ -0,0 +1,39 @@ +/* + * 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 { deepFreeze } from '@kbn/std'; +import { + DATA_COLLECTION, + EDR_COMPLETE, + EDR_ESSENTIAL, + EDR_NOTE, + NGAV, + NGAV_NOTE, +} from './translations'; + +export const ENDPOINT_INTEGRATION_CONFIG_KEY = 'ENDPOINT_INTEGRATION_CONFIG'; + +export const endpointPresetsMapping = deepFreeze({ + NGAV: { + label: NGAV, + note: NGAV_NOTE, + }, + EDREssential: { + label: EDR_ESSENTIAL, + note: EDR_NOTE, + }, + EDRComplete: { + label: EDR_COMPLETE, + note: EDR_NOTE, + }, + DataCollection: { + label: DATA_COLLECTION, + note: null, + }, +}); + +export type EndpointPreset = keyof typeof endpointPresetsMapping; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/endpoint_policy_create_extension.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/endpoint_policy_create_extension.test.tsx index d1875b4492a04..cfbdcfd12c26e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/endpoint_policy_create_extension.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/endpoint_policy_create_extension.test.tsx @@ -17,6 +17,7 @@ import type { import type { AppContextTestRender } from '../../../../../../common/mock/endpoint'; import { createAppRootMockRenderer } from '../../../../../../common/mock/endpoint'; import { licenseService } from '../../../../../../common/hooks/use_license'; +import type { PackagePolicyCreateExtensionComponentProps } from '@kbn/fleet-plugin/public'; jest.mock('../../../../../../common/lib/kibana'); jest.mock('../../../../../../common/hooks/use_license', () => { @@ -59,8 +60,7 @@ const getMockNewPackage = (): NewPackagePolicy => { }; describe('Onboarding Component new section', () => { - let render: () => ReturnType; - let renderResult: ReturnType; + let renderResult: ReturnType; let mockedContext: AppContextTestRender; beforeEach(() => { @@ -159,4 +159,107 @@ describe('Onboarding Component new section', () => { }); }); }); + + describe('when policy protections are not available', () => { + let newPolicy: NewPackagePolicy; + let onChange: PackagePolicyCreateExtensionComponentProps['onChange']; + let render: () => ReturnType; + + beforeEach(() => { + mockedContext.startServices.upselling.registerSections({ + endpointPolicyProtections: () =>
{'pay up!'}
, + }); + newPolicy = getMockNewPackage(); + onChange = jest.fn(); + render = () => { + renderResult = mockedContext.render( + + ); + return renderResult; + }; + }); + + it('should render expected preset for endpoint', () => { + const { getByTestId } = render(); + + expect(getByTestId('endpointDataCollectionOnlyPreset')).toHaveTextContent( + 'Data Collection' + + 'Augment your existing anti-virus solution with advanced data collection and detection' + + 'pay up!' + ); + }); + + it('should set the correct value for preset in policy', () => { + render(); + + expect(onChange).toHaveBeenLastCalledWith({ + isValid: true, + updatedPolicy: { + enabled: true, + id: 'someid', + inputs: [ + { + config: { + _config: { + value: { + endpointConfig: { + preset: 'DataCollection', + }, + type: 'endpoint', + }, + }, + }, + enabled: true, + streams: [], + type: 'ENDPOINT_INTEGRATION_CONFIG', + }, + ], + name: 'someName', + namespace: 'someNamespace', + policy_id: 'somePolicyid', + }, + }); + }); + + it('should still be able to select cloud configuration', () => { + render(); + userEvent.selectOptions(screen.getByTestId('selectIntegrationTypeId'), ['cloud']); + + expect(onChange).toHaveBeenLastCalledWith({ + isValid: true, + updatedPolicy: { + enabled: true, + id: 'someid', + inputs: [ + { + config: { + _config: { + value: { + eventFilters: { + nonInteractiveSession: true, + }, + type: 'cloud', + }, + }, + }, + enabled: true, + streams: [ + { + data_stream: { + dataset: 'someDataset', + type: 'someType', + }, + enabled: true, + }, + ], + type: 'someType', + }, + ], + name: 'someName', + namespace: 'someNamespace', + policy_id: 'somePolicyid', + }, + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/endpoint_policy_create_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/endpoint_policy_create_extension.tsx index b67e1d6d0dca2..d9349eb6d6f5c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/endpoint_policy_create_extension.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/endpoint_policy_create_extension.tsx @@ -5,70 +5,47 @@ * 2.0. */ -import React, { memo, useState, useEffect, useCallback } from 'react'; +import React, { memo, useCallback, useEffect, useState } from 'react'; import { + EuiCallOut, + EuiCode, EuiForm, + EuiFormRow, + EuiLink, EuiRadio, EuiSelect, + EuiSpacer, EuiText, EuiTitle, - EuiSpacer, - EuiFormRow, - EuiCallOut, - EuiLink, - EuiCode, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import styled from 'styled-components'; import type { PackagePolicyCreateExtensionComponentProps } from '@kbn/fleet-plugin/public'; +import type { EndpointPreset } from './constants'; +import { ENDPOINT_INTEGRATION_CONFIG_KEY, endpointPresetsMapping } from './constants'; +import { HelpTextWithPadding } from './components/help_text_with_padding'; +import { EndpointEventCollectionPreset } from './components/endpoint_event_collection_preset'; import { useLicense } from '../../../../../../common/hooks/use_license'; import { ALL_EVENTS, CLOUD_SECURITY, - EDR_COMPLETE, - NGAV, - EDR_ESSENTIAL, + DATA_COLLECTION_HELP_TEXT, ENDPOINT, INTERACTIVE_ONLY, - NGAV_NOTE, - EDR_NOTE, - DATA_COLLECTION, } from './translations'; +import { useGetProtectionsUnavailableComponent } from '../../policy_settings_form/hooks/use_get_protections_unavailable_component'; const PREFIX = 'endpoint_policy_create_extension'; -const ENDPOINT_INTEGRATION_CONFIG_KEY = 'ENDPOINT_INTEGRATION_CONFIG'; - const environmentMapping = { cloud: CLOUD_SECURITY, endpoint: ENDPOINT, }; -const endpointPresetsMapping = { - NGAV: { - label: NGAV, - note: NGAV_NOTE, - }, - EDREssential: { - label: EDR_ESSENTIAL, - note: EDR_NOTE, - }, - EDRComplete: { - label: EDR_COMPLETE, - note: EDR_NOTE, - }, - DataCollection: { - label: DATA_COLLECTION, - note: null, - }, -}; - const cloudEventMapping = { INTERACTIVE_ONLY, ALL_EVENTS, }; -type EndpointPreset = keyof typeof endpointPresetsMapping; type CloudEvent = keyof typeof cloudEventMapping; type Environment = keyof typeof environmentMapping; @@ -77,10 +54,6 @@ const environmentOptions: Array<{ value: Environment; text: string }> = [ { value: 'cloud', text: CLOUD_SECURITY }, ]; -const HelpTextWithPadding = styled.div` - padding-left: ${(props) => props.theme.eui.euiSizeL}; -`; - /** * Exports Endpoint-specific package policy instructions * for use in the Ingest app create / edit package policy @@ -89,6 +62,7 @@ export const EndpointPolicyCreateExtension = memo { const isPlatinumPlus = useLicense().isPlatinumPlus(); const isEnterprise = useLicense().isEnterprise(); + const showEndpointEventCollectionOnlyPreset = Boolean(useGetProtectionsUnavailableComponent()); const [endpointPreset, setEndpointPreset] = useState('EDRComplete'); const [selectedCloudEvent, setSelectedCloudEvent] = useState('INTERACTIVE_ONLY'); @@ -105,63 +79,77 @@ export const EndpointPolicyCreateExtension = memo { - if (newPolicy.inputs.length === 0) { - onChange({ - isValid: false, - updatedPolicy: { - ...newPolicy, - name: '', - inputs: [ - { - enabled: true, - streams: [], - type: ENDPOINT_INTEGRATION_CONFIG_KEY, - config: { - _config: { - value: { - type: 'endpoint', - endpointConfig: { - preset: 'NGAV', + // When ONLY Data collection is allowed, the updates to the policy are handled by the + // EndpointEventCollectionPreset component + if ( + !showEndpointEventCollectionOnlyPreset || + (showEndpointEventCollectionOnlyPreset && selectedEnvironment === 'cloud') + ) { + if (newPolicy.inputs.length === 0) { + onChange({ + isValid: false, + updatedPolicy: { + ...newPolicy, + name: '', + inputs: [ + { + enabled: true, + streams: [], + type: ENDPOINT_INTEGRATION_CONFIG_KEY, + config: { + _config: { + value: { + type: 'endpoint', + endpointConfig: { + preset: 'NGAV', + }, }, }, }, }, - }, - ], - }, - }); - } else { - onChange({ - isValid: true, - updatedPolicy: { - ...newPolicy, - inputs: [ - { - ...newPolicy.inputs[0], - config: { - _config: { - value: { - type: selectedEnvironment, - ...(selectedEnvironment === 'cloud' - ? { - eventFilters: { - nonInteractiveSession: selectedCloudEvent === 'INTERACTIVE_ONLY', - }, - } - : { - endpointConfig: { - preset: endpointPreset, - }, - }), + ], + }, + }); + } else { + onChange({ + isValid: true, + updatedPolicy: { + ...newPolicy, + inputs: [ + { + ...newPolicy.inputs[0], + config: { + _config: { + value: { + type: selectedEnvironment, + ...(selectedEnvironment === 'cloud' + ? { + eventFilters: { + nonInteractiveSession: selectedCloudEvent === 'INTERACTIVE_ONLY', + }, + } + : { + endpointConfig: { + preset: endpointPreset, + }, + }), + }, }, }, }, - }, - ], - }, - }); + ], + }, + }); + } } - }, [selectedEnvironment, selectedCloudEvent, endpointPreset, onChange, newPolicy]); + }, [ + selectedEnvironment, + selectedCloudEvent, + endpointPreset, + onChange, + newPolicy, + showEndpointEventCollectionOnlyPreset, + ]); const onChangeEnvironment = useCallback((e: React.ChangeEvent) => { setSelectedEnvironment(e?.target?.value as Environment); @@ -243,94 +231,93 @@ export const EndpointPolicyCreateExtension = memo + {selectedEnvironment === 'endpoint' ? ( - <> - - - - - } - > - - - - - - - } - > - - - - - - - } - > - - - - - - - } - > - - - {showNote && ( - <> - - - -

- {endpointPresetsMapping[endpointPreset].note}{' '} - - - - ), - }} - /> -

-
-
- - )} - + !showEndpointEventCollectionOnlyPreset ? ( + <> + + {DATA_COLLECTION_HELP_TEXT}} + > + + + + + + + } + > + + + + + + + } + > + + + + + + + } + > + + + + {showNote && ( + <> + + + +

+ {endpointPresetsMapping[endpointPreset].note}{' '} + + + + ), + }} + /> +

+
+
+ + )} + + ) : ( + + ) ) : ( <> diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/translations.ts b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/translations.ts index 3772924fafd1d..e79e06d55e65b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/endpoint_policy_create_extension/translations.ts @@ -49,6 +49,14 @@ export const DATA_COLLECTION = i18n.translate( } ); +export const DATA_COLLECTION_HELP_TEXT = i18n.translate( + 'xpack.securitySolution.createPackagePolicy.stepConfigure.packagePolicyTypeEndpointDataCollection', + { + defaultMessage: + 'Augment your existing anti-virus solution with advanced data collection and detection', + } +); + export const ENDPOINT = i18n.translate( 'xpack.securitySolution.createPackagePolicy.stepConfigure.endpointDropdownOption', { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension.tsx index 840abd074eb5c..5955661c82864 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension.tsx @@ -7,12 +7,26 @@ import { lazy } from 'react'; import type { PackagePolicyCreateExtensionComponent } from '@kbn/fleet-plugin/public'; +import type { FleetUiExtensionGetterOptions } from './types'; + +export const getLazyEndpointPolicyCreateExtension = ({ + coreStart, + depsStart, + services, +}: FleetUiExtensionGetterOptions) => { + return lazy(async () => { + const [{ withSecurityContext }, { EndpointPolicyCreateExtension }] = await Promise.all([ + import('./components/with_security_context/with_security_context'), + import('./endpoint_policy_create_extension'), + ]); -export const LazyEndpointPolicyCreateExtension = lazy( - async () => { - const { EndpointPolicyCreateExtension } = await import('./endpoint_policy_create_extension'); return { - default: EndpointPolicyCreateExtension, + default: withSecurityContext({ + coreStart, + depsStart, + services, + WrappedComponent: EndpointPolicyCreateExtension, + }), }; - } -); + }); +}; diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index f619dead10399..2ebe399c64f6a 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -44,7 +44,7 @@ import type { SecuritySolutionUiConfigType } from './common/types'; import { ExperimentalFeaturesService } from './common/experimental_features_service'; import { getLazyEndpointPolicyEditExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_edit_extension'; -import { LazyEndpointPolicyCreateExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension'; +import { getLazyEndpointPolicyCreateExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_extension'; import { LazyEndpointPolicyCreateMultiStepExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_create_multi_step_extension'; import { getLazyEndpointPackageCustomExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_package_custom_extension'; import { getLazyEndpointPolicyResponseExtension } from './management/pages/policy/view/ingest_manager_integration/lazy_endpoint_policy_response_extension'; @@ -306,7 +306,7 @@ export class Plugin implements IPlugin => { export type ActionConnector = Omit; export const fetchConnectors = async (): Promise => { - const response = (await apiService.get(SYNTHETICS_API_URLS.RULE_CONNECTORS)) as Array< - AsApiContract - >; - return response.map( - ({ - connector_type_id: actionTypeId, - referenced_by_count: referencedByCount, - is_preconfigured: isPreconfigured, - is_deprecated: isDeprecated, - is_missing_secrets: isMissingSecrets, - is_system_action: isSystemAction, - ...res - }) => ({ - ...res, - actionTypeId, - referencedByCount, - isDeprecated, - isPreconfigured, - isMissingSecrets, - isSystemAction, - }) - ); + return await apiService.get(SYNTHETICS_API_URLS.GET_ACTIONS_CONNECTORS); }; export const fetchActionTypes = async (): Promise => { - const response = (await apiService.get(SYNTHETICS_API_URLS.CONNECTOR_TYPES, { + return await apiService.get(SYNTHETICS_API_URLS.GET_CONNECTOR_TYPES, { feature_id: 'uptime', - })) as Array>; - return response.map( - ({ - enabled_in_config: enabledInConfig, - enabled_in_license: enabledInLicense, - minimum_license_required: minimumLicenseRequired, - supported_feature_ids: supportedFeatureIds, - is_system_action_type: isSystemActionType, - ...res - }: AsApiContract) => ({ - ...res, - enabledInConfig, - enabledInLicense, - minimumLicenseRequired, - supportedFeatureIds, - isSystemActionType, - }) - ); + }); }; export const syncGlobalParamsAPI = async (): Promise => { diff --git a/x-pack/plugins/synthetics/server/lib.ts b/x-pack/plugins/synthetics/server/lib.ts index 4dc06bc7b9674..6ee70f5282e83 100644 --- a/x-pack/plugins/synthetics/server/lib.ts +++ b/x-pack/plugins/synthetics/server/lib.ts @@ -170,7 +170,7 @@ export class UptimeEsClient { const showInspectData = (isInspectorEnabled || this.isDev) && path !== SYNTHETICS_API_URLS.DYNAMIC_SETTINGS; - if (showInspectData) { + if (showInspectData && this.inspectableEsQueries.length > 0) { return { _inspect: this.inspectableEsQueries }; } return {}; diff --git a/x-pack/plugins/synthetics/server/routes/default_alerts/get_action_connectors.ts b/x-pack/plugins/synthetics/server/routes/default_alerts/get_action_connectors.ts new file mode 100644 index 0000000000000..c98a19a2e9335 --- /dev/null +++ b/x-pack/plugins/synthetics/server/routes/default_alerts/get_action_connectors.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 { SyntheticsRestApiRouteFactory } from '../types'; +import { SYNTHETICS_API_URLS } from '../../../common/constants'; + +export const getActionConnectorsRoute: SyntheticsRestApiRouteFactory = () => ({ + method: 'GET', + path: SYNTHETICS_API_URLS.GET_ACTIONS_CONNECTORS, + validate: {}, + handler: async ({ context, server, savedObjectsClient }): Promise => { + const actionsClient = (await context.actions)?.getActionsClient(); + + return actionsClient.getAll(); + }, +}); diff --git a/x-pack/plugins/synthetics/server/routes/default_alerts/get_connector_types.ts b/x-pack/plugins/synthetics/server/routes/default_alerts/get_connector_types.ts new file mode 100644 index 0000000000000..7d829090e84c6 --- /dev/null +++ b/x-pack/plugins/synthetics/server/routes/default_alerts/get_connector_types.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 { SyntheticsRestApiRouteFactory } from '../types'; +import { SYNTHETICS_API_URLS } from '../../../common/constants'; + +export const getConnectorTypesRoute: SyntheticsRestApiRouteFactory = () => ({ + method: 'GET', + path: SYNTHETICS_API_URLS.GET_CONNECTOR_TYPES, + validate: {}, + handler: async ({ context, server, savedObjectsClient }): Promise => { + const actionsClient = (await context.actions)?.getActionsClient(); + + return actionsClient.listTypes({ featureId: 'uptime' }); + }, +}); diff --git a/x-pack/plugins/synthetics/server/routes/index.ts b/x-pack/plugins/synthetics/server/routes/index.ts index 8fe8ad595f2ee..accad22a12817 100644 --- a/x-pack/plugins/synthetics/server/routes/index.ts +++ b/x-pack/plugins/synthetics/server/routes/index.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { getConnectorTypesRoute } from './default_alerts/get_connector_types'; +import { getActionConnectorsRoute } from './default_alerts/get_action_connectors'; import { SyntheticsRestApiRouteFactory } from './types'; import { getSyntheticsCertsRoute } from './certs/get_certificates'; import { getAgentPoliciesRoute } from './settings/private_locations/get_agent_policies'; @@ -100,4 +102,6 @@ export const syntheticsAppRestApiRoutes: SyntheticsRestApiRouteFactory[] = [ inspectSyntheticsMonitorRoute, getAgentPoliciesRoute, getSyntheticsCertsRoute, + getActionConnectorsRoute, + getConnectorTypesRoute, ]; diff --git a/x-pack/plugins/synthetics/server/synthetics_route_wrapper.ts b/x-pack/plugins/synthetics/server/synthetics_route_wrapper.ts index 7b4b67f63207f..e58aae2ccd121 100644 --- a/x-pack/plugins/synthetics/server/synthetics_route_wrapper.ts +++ b/x-pack/plugins/synthetics/server/synthetics_route_wrapper.ts @@ -6,6 +6,7 @@ */ import { KibanaResponse } from '@kbn/core-http-router-server-internal'; import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; +import { isEmpty } from 'lodash'; import { isTestUser, UptimeEsClient } from './lib'; import { checkIndicesReadPrivileges } from './synthetics_service/authentication/check_has_privilege'; import { SYNTHETICS_INDEX_PATTERN } from '../common/constants'; @@ -59,6 +60,23 @@ export const syntheticsRouteWrapper: SyntheticsRouteWrapper = ( return res; } + const inspectData = await uptimeEsClient.getInspectData(uptimeRoute.path); + + if (Array.isArray(res)) { + if (isEmpty(inspectData)) { + return response.ok({ + body: res, + }); + } else { + return response.ok({ + body: { + result: res, + ...inspectData, + }, + }); + } + } + return response.ok({ body: { ...res, diff --git a/x-pack/plugins/watcher/server/index.ts b/x-pack/plugins/watcher/server/index.ts index 36453f571f162..874575ef9c122 100644 --- a/x-pack/plugins/watcher/server/index.ts +++ b/x-pack/plugins/watcher/server/index.ts @@ -14,6 +14,13 @@ export const plugin = (ctx: PluginInitializerContext) => new WatcherServerPlugin export const config = { schema: schema.object({ - enabled: schema.boolean({ defaultValue: true }), + enabled: schema.conditional( + schema.contextRef('serverless'), + true, + // Watcher is disabled in serverless; refer to the serverless.yml file as the source of truth + // We take this approach in order to have a central place (serverless.yml) to view disabled plugins across Kibana + schema.boolean({ defaultValue: true }), + schema.never() + ), }), }; diff --git a/x-pack/test/functional/apps/infra/hosts_view.ts b/x-pack/test/functional/apps/infra/hosts_view.ts index d909e493bf2c5..f5dc470587c37 100644 --- a/x-pack/test/functional/apps/infra/hosts_view.ts +++ b/x-pack/test/functional/apps/infra/hosts_view.ts @@ -155,7 +155,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { return !!currentUrl.match(path); }); - describe('Hosts View', function () { + // Failing: See https://github.com/elastic/kibana/issues/162672 + describe.skip('Hosts View', function () { before(async () => { await Promise.all([ esArchiver.load('x-pack/test/functional/es_archives/infra/alerts'), diff --git a/x-pack/test/functional/apps/upgrade_assistant/deprecation_pages.ts b/x-pack/test/functional/apps/upgrade_assistant/deprecation_pages.ts index 1eb4752026b75..19b4fad72d272 100644 --- a/x-pack/test/functional/apps/upgrade_assistant/deprecation_pages.ts +++ b/x-pack/test/functional/apps/upgrade_assistant/deprecation_pages.ts @@ -6,7 +6,6 @@ */ import expect from '@kbn/expect'; -import { setTimeout } from 'timers/promises'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function upgradeAssistantFunctionalTests({ @@ -20,50 +19,27 @@ export default function upgradeAssistantFunctionalTests({ const security = getService('security'); const log = getService('log'); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/160833 - describe.skip('Deprecation pages', function () { - this.tags('skipFirefox'); + describe('Deprecation pages', function () { + this.tags(['skipFirefox', 'upgradeAssistant']); before(async () => { await security.testUser.setRoles(['superuser']); - - // Cluster readiness checks try { - // Trigger "Total shards" ES Upgrade readiness check + /** + * Trigger "Total shards" ES Upgrade readiness check + * the number of shards in the test cluster is 25-27 + * so 5 max shards per node should trigger this check + * on both local and CI environments. + */ await es.cluster.putSettings({ body: { persistent: { cluster: { - max_shards_per_node: '9', + max_shards_per_node: 5, }, }, }, }); - - // Trigger "Low disk watermark" ES Upgrade readiness check - await es.cluster.putSettings({ - body: { - persistent: { - cluster: { - // push allocation changes to nodes quickly during tests - info: { - update: { interval: '10s' }, - }, - routing: { - allocation: { - disk: { - threshold_enabled: true, - watermark: { low: '30%' }, - }, - }, - }, - }, - }, - }, - }); - - // Wait for the cluster settings to be reflected to the ES nodes - await setTimeout(12000); } catch (e) { log.debug('[Setup error] Error updating cluster settings'); throw e; @@ -76,18 +52,8 @@ export default function upgradeAssistantFunctionalTests({ body: { persistent: { cluster: { - info: { - update: { interval: null }, - }, - max_shards_per_node: null, - routing: { - allocation: { - disk: { - threshold_enabled: false, - watermark: { low: null }, - }, - }, - }, + // initial cluster setting from x-pack/test/functional/config.upgrade_assistant.js + max_shards_per_node: 27, }, }, }, @@ -96,7 +62,6 @@ export default function upgradeAssistantFunctionalTests({ log.debug('[Cleanup error] Error reseting cluster settings'); throw e; } - await security.testUser.restoreDefaults(); }); @@ -111,10 +76,13 @@ export default function upgradeAssistantFunctionalTests({ it('renders the Elasticsearch upgrade readiness deprecations', async () => { const deprecationMessages = await testSubjects.getVisibleTextAll('defaultTableCell-message'); + const healthIndicatorsCriticalMessages = await testSubjects.getVisibleTextAll( + 'healthIndicatorTableCell-message' + ); expect(deprecationMessages).to.contain('Disk usage exceeds low watermark'); - expect(deprecationMessages).to.contain( - 'The cluster has too many shards to be able to upgrade' + expect(healthIndicatorsCriticalMessages).to.contain( + 'Elasticsearch is about to reach the maximum number of shards it can host, based on your current settings.' ); }); diff --git a/x-pack/test/functional/apps/upgrade_assistant/es_deprecation_logs_page.ts b/x-pack/test/functional/apps/upgrade_assistant/es_deprecation_logs_page.ts index 2ba3531486fff..647d692299075 100644 --- a/x-pack/test/functional/apps/upgrade_assistant/es_deprecation_logs_page.ts +++ b/x-pack/test/functional/apps/upgrade_assistant/es_deprecation_logs_page.ts @@ -17,7 +17,7 @@ export default function upgradeAssistantESDeprecationLogsPageFunctionalTests({ const es = getService('es'); describe('ES deprecation logs page', function () { - this.tags('skipFirefox'); + this.tags(['skipFirefox', 'upgradeAssistant']); before(async () => { await security.testUser.setRoles(['superuser']); diff --git a/x-pack/test/functional/apps/upgrade_assistant/feature_controls/upgrade_assistant_security.ts b/x-pack/test/functional/apps/upgrade_assistant/feature_controls/upgrade_assistant_security.ts index 755f69cb43c20..a7c883733ea13 100644 --- a/x-pack/test/functional/apps/upgrade_assistant/feature_controls/upgrade_assistant_security.ts +++ b/x-pack/test/functional/apps/upgrade_assistant/feature_controls/upgrade_assistant_security.ts @@ -15,6 +15,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const managementMenu = getService('managementMenu'); describe('security', function () { + this.tags('upgradeAssistant'); before(async () => { await PageObjects.common.navigateToApp('home'); }); diff --git a/x-pack/test/functional/apps/upgrade_assistant/overview_page.ts b/x-pack/test/functional/apps/upgrade_assistant/overview_page.ts index 6629ec6b9cd67..10493d1df8117 100644 --- a/x-pack/test/functional/apps/upgrade_assistant/overview_page.ts +++ b/x-pack/test/functional/apps/upgrade_assistant/overview_page.ts @@ -16,7 +16,7 @@ export default function upgradeAssistantOverviewPageFunctionalTests({ const testSubjects = getService('testSubjects'); describe('Overview Page', function () { - this.tags('skipFirefox'); + this.tags(['skipFirefox', 'upgradeAssistant']); before(async () => { await security.testUser.setRoles(['superuser']); diff --git a/x-pack/test/functional/config.base.js b/x-pack/test/functional/config.base.js index e4427d66e534d..36dc0c6d84c3c 100644 --- a/x-pack/test/functional/config.base.js +++ b/x-pack/test/functional/config.base.js @@ -35,11 +35,7 @@ export default async function ({ readConfigFile }) { esTestCluster: { license: 'trial', from: 'snapshot', - serverArgs: [ - 'path.repo=/tmp/', - 'xpack.security.authc.api_key.enabled=true', - 'cluster.routing.allocation.disk.threshold_enabled=true', // make sure disk thresholds are enabled for UA cluster testing - ], + serverArgs: ['path.repo=/tmp/', 'xpack.security.authc.api_key.enabled=true'], }, kbnTestServer: { @@ -183,6 +179,11 @@ export default async function ({ readConfigFile }) { }, }, + suiteTags: { + ...kibanaCommonConfig.get('suiteTags'), + exclude: [...kibanaCommonConfig.get('suiteTags').exclude, 'upgradeAssistant'], + }, + // choose where screenshots should be saved screenshots: { directory: resolve(__dirname, 'screenshots'), diff --git a/x-pack/test/functional/config.upgrade_assistant.ts b/x-pack/test/functional/config.upgrade_assistant.ts new file mode 100644 index 0000000000000..a9e0a447a2961 --- /dev/null +++ b/x-pack/test/functional/config.upgrade_assistant.ts @@ -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 { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('./config.base.js')); + + return { + ...functionalConfig.getAll(), + + testFiles: [require.resolve('./apps/upgrade_assistant')], + + junit: { + reportName: 'Chrome X-Pack UI Upgrade Assistant Functional Tests', + }, + + suiteTags: { + include: ['upgradeAssistant'], + }, + + esTestCluster: { + ...functionalConfig.get('esTestCluster'), + serverArgs: [ + 'path.repo=/tmp/', + 'xpack.security.authc.api_key.enabled=true', + 'cluster.routing.allocation.disk.threshold_enabled=true', // make sure disk thresholds are enabled for UA cluster testing + 'cluster.routing.allocation.disk.watermark.low=30%', + 'cluster.info.update.interval=10s', + 'cluster.max_shards_per_node=27', + ], + }, + }; +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/index.ts index 7f5c93b0dbaff..de30854beccfc 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/index.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/index.ts @@ -12,5 +12,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./security_users')); loadTestFile(require.resolve('./spaces')); loadTestFile(require.resolve('./security_response_headers')); + loadTestFile(require.resolve('./rollups')); }); } diff --git a/x-pack/test_serverless/api_integration/test_suites/common/rollups.ts b/x-pack/test_serverless/api_integration/test_suites/common/rollups.ts new file mode 100644 index 0000000000000..47dd58f242759 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/rollups.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from 'expect'; +import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common'; +import { INITIAL_REST_VERSION_INTERNAL } from '@kbn/data-views-plugin/server/constants'; +import { X_ELASTIC_INTERNAL_ORIGIN_REQUEST } from '@kbn/core-http-common/src/constants'; +import { FIELDS_FOR_WILDCARD_PATH as BASE_URI } from '@kbn/data-views-plugin/common/constants'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('rollup data views - fields for wildcard', function () { + before(async () => { + await esArchiver.load('test/api_integration/fixtures/es_archiver/index_patterns/basic_index'); + }); + + after(async () => { + await esArchiver.unload( + 'test/api_integration/fixtures/es_archiver/index_patterns/basic_index' + ); + }); + it('returns 200 and best effort response despite lack of rollup support', async () => { + const response = await supertest + .get(BASE_URI) + .query({ + pattern: 'basic_index', + type: 'rollup', + rollup_index: 'bar', + }) + .set(ELASTIC_HTTP_VERSION_HEADER, INITIAL_REST_VERSION_INTERNAL) + .set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'true'); + + expect(response.status).toBe(200); + expect(response.body.fields.length).toEqual(5); + }); + }); +} diff --git a/x-pack/test_serverless/functional/test_suites/common/management.ts b/x-pack/test_serverless/functional/test_suites/common/management.ts index 0b2f2cab24716..504325ff363b1 100644 --- a/x-pack/test_serverless/functional/test_suites/common/management.ts +++ b/x-pack/test_serverless/functional/test_suites/common/management.ts @@ -50,6 +50,10 @@ export default function ({ getPageObject, getService }: FtrProviderContext) { appName: 'License Management', url: 'stack/license_management', }, + { + appName: 'Watcher', + url: 'insightsAndAlerting/watcher', + }, ]; DISABLED_PLUGINS.forEach(({ appName, url }) => { diff --git a/x-pack/test_serverless/tsconfig.json b/x-pack/test_serverless/tsconfig.json index 0b9c8cb7792a9..8c431b2537986 100644 --- a/x-pack/test_serverless/tsconfig.json +++ b/x-pack/test_serverless/tsconfig.json @@ -44,5 +44,7 @@ "@kbn/fleet-plugin", "@kbn/cases-plugin", "@kbn/test-subj-selector", + "@kbn/core-http-common", + "@kbn/data-views-plugin", ] }