From 3dfd0511430d89fcd2704a06f7b4a66d31d3ab2f Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Wed, 29 Jan 2025 10:39:13 -0800 Subject: [PATCH 01/12] [Dashboard] Replace deprecated colors in Borealis (#208473) ## Summary Closes https://github.com/elastic/kibana/issues/204590. This replaces the remaining color tokens that were deprecated in the Borealis theme in the Presentation apps. All others are using valid color tokens. ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) ### Identify risks Does this PR introduce any risks? For example, consider risks like hard to test bugs, performance regression, potential of data loss. Describe the risk, its severity, and mitigation for each identified risk. Invite stakeholders and evaluate how to proceed before merging. - [ ] [See some risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) - [ ] ... --- .../top_nav/add_panel_button/components/add_panel_flyout.tsx | 4 ++-- .../public/components/dashboard_picker/dashboard_picker.tsx | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/add_panel_button/components/add_panel_flyout.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/add_panel_button/components/add_panel_flyout.tsx index 4607cd35d9e0e..305495b69599e 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/add_panel_button/components/add_panel_flyout.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/add_panel_button/components/add_panel_flyout.tsx @@ -104,11 +104,11 @@ export function AddPanelFlyout({ dashboardApi }: { dashboardApi: DashboardApi }) position: 'sticky', top: euiTheme.size.m, zIndex: 1, - boxShadow: `0 -${euiTheme.size.m} 0 4px ${euiTheme.colors.emptyShade}`, + boxShadow: `0 -${euiTheme.size.m} 0 4px ${euiTheme.colors.backgroundBasePlain}`, }} > - + {selectedDashboard?.label ?? From a115b8e1f580bf6a9812cf0dd2c51dd09837eea5 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 29 Jan 2025 19:31:58 +0000 Subject: [PATCH 02/12] skip flaky suite (#208380) --- .../shared/cases/public/containers/use_get_case_users.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/cases/public/containers/use_get_case_users.test.tsx b/x-pack/platform/plugins/shared/cases/public/containers/use_get_case_users.test.tsx index 7f425081989b8..efaf5dca6f1ab 100644 --- a/x-pack/platform/plugins/shared/cases/public/containers/use_get_case_users.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/containers/use_get_case_users.test.tsx @@ -15,7 +15,8 @@ import { createAppMockRenderer } from '../common/mock'; jest.mock('./api'); jest.mock('../common/lib/kibana'); -describe('useGetCaseUsers', () => { +// FLAKY: https://github.com/elastic/kibana/issues/208380 +describe.skip('useGetCaseUsers', () => { let appMockRender: AppMockRenderer; beforeEach(() => { From c9e0f28fa2e6db1e317b6210d9452728dd703e08 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 29 Jan 2025 19:33:07 +0000 Subject: [PATCH 03/12] skip flaky suite (#207248) --- .../cases/public/containers/use_post_push_to_service.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/cases/public/containers/use_post_push_to_service.test.tsx b/x-pack/platform/plugins/shared/cases/public/containers/use_post_push_to_service.test.tsx index 0c7a76bc6ff3a..d351eaa12e36a 100644 --- a/x-pack/platform/plugins/shared/cases/public/containers/use_post_push_to_service.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/containers/use_post_push_to_service.test.tsx @@ -19,7 +19,8 @@ import { casesQueriesKeys } from './constants'; jest.mock('./api'); jest.mock('../common/lib/kibana'); -describe('usePostPushToService', () => { +// FLAKY: https://github.com/elastic/kibana/issues/207248 +describe.skip('usePostPushToService', () => { const connector = { id: '123', name: 'My connector', From 9c1b849556b715d544c722e5b0f57b5f6ff90d9e Mon Sep 17 00:00:00 2001 From: Elena Shostak <165678770+elena-shostak@users.noreply.github.com> Date: Wed, 29 Jan 2025 20:33:36 +0100 Subject: [PATCH 04/12] [FTR] Skipped tests for FIPS (#208759) ## Summary ## Summary All tests in `x-pack/test/spaces_api_integration/deployment_agnostic/security_and_spaces/stateful.config_basic.ts` are intended to be run only with `basic` license, since FIPS overrides it we need to skip that test for FIPS. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../security_and_spaces/apis/index.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/x-pack/test/spaces_api_integration/deployment_agnostic/security_and_spaces/apis/index.ts b/x-pack/test/spaces_api_integration/deployment_agnostic/security_and_spaces/apis/index.ts index 392ae720c9330..fc78bc364fe35 100644 --- a/x-pack/test/spaces_api_integration/deployment_agnostic/security_and_spaces/apis/index.ts +++ b/x-pack/test/spaces_api_integration/deployment_agnostic/security_and_spaces/apis/index.ts @@ -13,11 +13,16 @@ export default function ({ loadTestFile, getService }: DeploymentAgnosticFtrProv const license = config.get('esTestCluster.license'); const es = getService('es'); const supertest = getService('supertest'); + // Should we enabled when custom roles can be provisioned for MKI + // See: https://github.com/elastic/kibana/issues/207361 + const tags = ['skipMKI']; + + if (license === 'basic') { + tags.push('skipFIPS'); + } describe('spaces api with security', function () { - // Should we enabled when custom roles can be provisioned for MKI - // See: https://github.com/elastic/kibana/issues/207361 - this.tags('skipMKI'); + this.tags(tags); before(async () => { if (license === 'basic') { await createUsersAndRoles(es, supertest); From 138145411d9663e9673cd7bb3d3d0f40d5027c9d Mon Sep 17 00:00:00 2001 From: Gerard Soldevila Date: Wed, 29 Jan 2025 20:34:00 +0100 Subject: [PATCH 05/12] SKA: Update repository structure documentation (#208691) ## Summary * Updates [Repository structure](https://docs.elastic.dev/kibana-dev-docs/contributing/repo-structure) docs * Makes the `osquery` plugin eslint exception more specific. --- .eslintrc.js | 4 ++- dev_docs/contributing/code_walkthrough.mdx | 31 +++++++++++----------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 98a33bce61b12..47927959d60ed 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2030,7 +2030,9 @@ module.exports = { 'src/cli_setup/**', // is importing "@kbn/interactive-setup-plugin" (platform/private) 'src/dev/build/tasks/install_chromium.ts', // is importing "@kbn/screenshotting-plugin" (platform/private) - // @kbn/osquery-plugin could be categorised as Security, but @kbn/infra-plugin (observability) depends on it! + // FIXME @kbn/osquery-plugin has dependencies on: + // - @kbn/timelines-plugin (security/private) https://github.com/elastic/kibana/blob/main/x-pack/platform/plugins/shared/osquery/public/types.ts#L20 + // - @kbn/security-solution-plugin (security/private) this one is “less critical” as it is cypress depending on cypress 'x-pack/platform/plugins/shared/osquery/**', // For now, we keep the exception to let tests depend on anythying. diff --git a/dev_docs/contributing/code_walkthrough.mdx b/dev_docs/contributing/code_walkthrough.mdx index 735740a9397b9..388446f79e67e 100644 --- a/dev_docs/contributing/code_walkthrough.mdx +++ b/dev_docs/contributing/code_walkthrough.mdx @@ -52,10 +52,6 @@ We used to write RFCs in `md` format and keep them in the repository, but we hav Contains our two license header texts, one for the Elastic license and one for the Elastic+SSPL license. All code files inside x-pack should have the Elastic license text at the top, all code files outside x-pack should contain the other. If you have your environment set up to auto-fix on save, eslint should take care of adding it for you. If you don't have it, ci will fail. Can be ignored for the most part, this rarely changes. -## [packages](https://github.com/elastic/kibana/tree/main/packages) - -The packages folder contains a mixture of build-time related code (like the [code needed to build the api docs](https://github.com/elastic/kibana/tree/main/packages/kbn-docs-utils)), as well as static code that some plugins rely on (like the [kbn-monaco package](https://github.com/elastic/kibana/tree/main/src/platform/packages/shared/kbn-monaco)). covers how packages differ from plugins. - ## [plugins](https://github.com/elastic/kibana/tree/main/plugins) This is an empty folder in GitHub. It's where third party developers should put their plugin folders. Internal developers can ignore this folder. @@ -83,13 +79,15 @@ This code primarily belongs to the Core team and contains the plugin infrastruct ### [src/dev](https://github.com/elastic/kibana/tree/main/src/dev) -Maintained by the Operations team, this code contains build and development tooling related code. This folder existed before `packages`, so contains mostly older code that hasn't been migrated to packages. Prefer creating a `package` if possible. Can be ignored for the most part if you are not on the Ops team. +Maintained by the Operations team, this code contains build and development tooling related code. This folder existed before `packages`, so contains mostly older code that hasn't been migrated to packages. Prefer creating a `package` if possible. Can be ignored for the most part if you are not on the Ops team. + +### [src/platform](https://github.com/elastic/kibana/tree/main/src/platform) -### [src/plugins](https://github.com/elastic/kibana/tree/main/src/plugins) +Contains the Basic-licensed code that is common to all Kibana solutions. The code is organised in modules, that can be either [plugins](https://github.com/elastic/kibana/tree/main/src/platform/plugins) or [packages](https://github.com/elastic/kibana/tree/main/src/platform/packages). covers how packages differ from plugins. -Contains all of our Basic-licensed plugins. Most folders in this directory will contain `README.md` files explaining what they do. If there are none, you can look at the `owner.gitHub` field inside all `kibana.json`s that will tell you which team to get in touch with for questions. +### [src/platform/plugins](https://github.com/elastic/kibana/tree/main/src/platform/plugins) -Note that as plugins can be nested, each folder in this directory may contain multiple plugins. +Contains Basic-licensed plugins that are common to all Kibana solutions. They are organized according to their _visibility_: Thy can be [shared](https://github.com/elastic/kibana/tree/main/src/platform/plugins/shared) or [private](https://github.com/elastic/kibana/tree/main/src/platform/plugins/private) (aka not accessible from solutions). ## [test](https://github.com/elastic/kibana/tree/main/test) @@ -99,13 +97,18 @@ Contains functional tests and related FTR (functional test runner) code for the Maintained by Ops and Core, this contains global typings for dependencies that do not provide their own types, or don't have types available via [DefinitelyTyped](https://definitelytyped.org). This directory is intended to be minimal; types should only be added here as a last resort. -## [vars](https://github.com/elastic/kibana/tree/main/vars) +## [x-pack](https://github.com/elastic/kibana/tree/main/x-pack) -A bunch of groovy scripts maintained by the Operations team. +Contains all code and infrasturcture that powers our gold+ (non-basic) features that are provided under a more restrictive license. The code is organized in the following subfolders: -## [x-pack](https://github.com/elastic/kibana/tree/main/x-pack) +## [x-pack/platform](https://github.com/elastic/kibana/tree/main/x-pack/platform) + +Contains all of the gold+ (non-basic) modules that are common to all Kibana solutions. Like the `src/platform` code, it is organized in modules that can be either plugins or packages, and in turn, these plugins are organized according to their visibility. -Contains all code and infrasturcture that powers our gold+ (non-basic) features that are provided under a more restrictive license. +## [x-pack/solutions](https://github.com/elastic/kibana/tree/main/x-pack/solutions) + +Contains all of the code that is specific to each Kibana solution. At the moment, we have a folder for [observability](https://github.com/elastic/kibana/tree/main/x-pack/solutions/observability), another for [security](https://github.com/elastic/kibana/tree/main/x-pack/solutions/security), and another for [search](https://github.com/elastic/kibana/tree/main/x-pack/solutions/search). These folders contain the modules that belong to each solution, and these modules are also categorised as plugins or packages. +Unlike the `src/platform` and the `x-pack/platform` code, the solution-specific modules are `private` by definition, aka they cannot be accessed from platform nor from other solutions. ### [x-pack/build_chromium](https://github.com/elastic/kibana/tree/main/x-pack/build_chromium) @@ -130,7 +133,3 @@ Maintained by the Ops team, this folder contains some scripts for running x-pack ### [x-pack/test](https://github.com/elastic/kibana/tree/main/x-pack/test) Functional tests for our gold+ features. - - - - From c0e430f5fbd7c6a4da5bbe624b6ec8deadd46889 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 29 Jan 2025 19:35:12 +0000 Subject: [PATCH 06/12] skip flaky suite (#207249) --- .../cases/public/components/create/owner_selector.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/cases/public/components/create/owner_selector.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/create/owner_selector.test.tsx index 8ba88a4d8a3c5..ee99b1a2bfc32 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/create/owner_selector.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/create/owner_selector.test.tsx @@ -13,7 +13,8 @@ import { OBSERVABILITY_OWNER, OWNER_INFO } from '../../../common/constants'; import { CreateCaseOwnerSelector } from './owner_selector'; import userEvent from '@testing-library/user-event'; -describe('Case Owner Selection', () => { +// FLAKY: https://github.com/elastic/kibana/issues/207249 +describe.skip('Case Owner Selection', () => { const onOwnerChange = jest.fn(); const selectedOwner = SECURITY_SOLUTION_OWNER; From fddf0aa7d19c6cb5a59139877c2e78de1d527743 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 29 Jan 2025 19:37:43 +0000 Subject: [PATCH 07/12] skip flaky suite (#208443) --- .../public/components/case_form_fields/connector.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/cases/public/components/case_form_fields/connector.test.tsx b/x-pack/platform/plugins/shared/cases/public/components/case_form_fields/connector.test.tsx index ddbbe02bc29ec..7bab779c9a96b 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/case_form_fields/connector.test.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/case_form_fields/connector.test.tsx @@ -49,7 +49,8 @@ const defaultProps = { isLoadingConnectors: false, }; -describe('Connector', () => { +// FLAKY: https://github.com/elastic/kibana/issues/208443 +describe.skip('Connector', () => { let appMockRender: AppMockRenderer; beforeEach(() => { From 5be4d61e9f8cc058fe843f6481464f6a7d7b5d3d Mon Sep 17 00:00:00 2001 From: "elastic-renovate-prod[bot]" <174716857+elastic-renovate-prod[bot]@users.noreply.github.com> Date: Wed, 29 Jan 2025 20:58:45 +0100 Subject: [PATCH 08/12] Update dependency @elastic/charts to v69.1.0 (main) (#208798) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [@elastic/charts](https://togithub.com/elastic/elastic-charts) | dependencies | minor | [`69.0.0` -> `69.1.0`](https://renovatebot.com/diffs/npm/@elastic%2fcharts/69.0.0/69.1.0) | --- ### Release Notes
elastic/elastic-charts (@​elastic/charts) ### [`v69.1.0`](https://togithub.com/elastic/elastic-charts/blob/HEAD/CHANGELOG.md#6910-2025-01-29) [Compare Source](https://togithub.com/elastic/elastic-charts/compare/v69.0.1...v69.1.0) ##### Bug Fixes - **deps:** update dependency json-schema-to-typescript to v15.0.4 ([#​2522](https://togithub.com/elastic/elastic-charts/issues/2522)) ([2d4b650](https://togithub.com/elastic/elastic-charts/commit/2d4b6505db822b1c9f6e7779978375630f96e2aa)) - **heatmap:** respect margins and paddings ([#​2577](https://togithub.com/elastic/elastic-charts/issues/2577)) ([c24566d](https://togithub.com/elastic/elastic-charts/commit/c24566d491d472caa0298a440d681e48fbf54992)) - **themes:** reintroduce Amsterdam colors ([#​2604](https://togithub.com/elastic/elastic-charts/issues/2604)) ([8c9913d](https://togithub.com/elastic/elastic-charts/commit/8c9913d2ec6b6594e0e6a89e03f047c45a894b22)) ##### Features - **heatmap:** add rotation in heatmap debug state ([#​2594](https://togithub.com/elastic/elastic-charts/issues/2594)) ([9047bd2](https://togithub.com/elastic/elastic-charts/commit/9047bd25583161eb80a58f45dd1f278e763acabb)) ### [`v69.0.1`](https://togithub.com/elastic/elastic-charts/releases/tag/v69.0.1) [Compare Source](https://togithub.com/elastic/elastic-charts/compare/v69.0.0...v69.0.1) ##### Bug Fixes - **themes:** reintroduce Amsterdam colors ([#​2604](https://togithub.com/elastic/elastic-charts/issues/2604)) \[69.0.x] ([#​2605](https://togithub.com/elastic/elastic-charts/issues/2605)) ([1b057d7](https://togithub.com/elastic/elastic-charts/commit/1b057d75dbedc0a5e0faa25052a3b6cf1033a483))
--- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Renovate Bot](https://togithub.com/renovatebot/renovate). Co-authored-by: elastic-renovate-prod[bot] <174716857+elastic-renovate-prod[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index bd0b1c3627c66..ac7d2a7dd4709 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,7 @@ "@elastic/apm-rum": "^5.16.3", "@elastic/apm-rum-core": "^5.22.1", "@elastic/apm-rum-react": "^2.0.5", - "@elastic/charts": "69.0.0", + "@elastic/charts": "69.1.0", "@elastic/datemath": "5.0.3", "@elastic/ebt": "^1.1.1", "@elastic/ecs": "^8.11.5", diff --git a/yarn.lock b/yarn.lock index e66768a124c6b..e7d2d090f0050 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2233,10 +2233,10 @@ dependencies: object-hash "^1.3.0" -"@elastic/charts@69.0.0": - version "69.0.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-69.0.0.tgz#c38531e6474ee64ebbad6d19b94fb543e5ee0033" - integrity sha512-TE7iRiGgLhYVImMpCFJleH91GinBlhbQKnVXtnjm+G8nYnvf6mA6Po7XViqKnEAUduAOVX8FJJJCL2/JWqRSJA== +"@elastic/charts@69.1.0": + version "69.1.0" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-69.1.0.tgz#5dff4f10f59a66ab291961ace141a99356751924" + integrity sha512-Ab1c/8i4wf3guFtMe7tK6ZClZ7TWHk6lVEAXdJJmKd5SCaXvGHrz8mZi9HkAQmooAtm54bnt43Nli8DfIigITw== dependencies: "@popperjs/core" "^2.11.8" bezier-easing "^2.1.0" From 95d863bc8b88402141ac44677fa8b81b9d29b0e1 Mon Sep 17 00:00:00 2001 From: Rodney Norris Date: Wed, 29 Jan 2025 14:24:38 -0600 Subject: [PATCH 09/12] [Search] [Onboarding] Hosted Quick Stats (#207925) ## Summary This PR updates the `search_indices` Index Details page to support quicks stats specific to stateful indices. ### Demo https://github.com/user-attachments/assets/5584f0b4-a7cb-4802-8aef-6708642a4629 ### Checklist - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed --------- Co-authored-by: Elastic Machine --- .../components/indices/details_page.tsx | 8 +- .../components/quick_stats/ai_search_stat.tsx | 84 +++++++ .../quick_stats/aliases_quick_stat.tsx | 52 +++++ .../components/quick_stats/constants.ts | 40 ++++ .../quick_stats/index_status_stat.tsx | 111 +++++++++ .../quick_stats/mappings_convertor.ts | 2 +- .../components/quick_stats/quick_stat.tsx | 45 ++-- .../components/quick_stats/quick_stats.tsx | 211 ++++++------------ .../quick_stats/quick_stats_container.tsx | 26 +++ .../quick_stats/setup_ai_search_button.tsx | 44 ++++ .../stateful_document_count_stat.tsx | 58 +++++ .../quick_stats/stateful_storage_stat.tsx | 54 +++++ .../stateless_document_cout_stat.tsx | 57 +++++ .../quick_stats/stateless_quick_stats.tsx | 28 +++ .../public/components/quick_stats/styles.ts | 29 +++ .../search_indices/public/utils/indices.ts | 20 ++ .../page_objects/search_index_details_page.ts | 19 +- .../tests/search_index_details.ts | 6 + 18 files changed, 726 insertions(+), 168 deletions(-) create mode 100644 x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/ai_search_stat.tsx create mode 100644 x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/aliases_quick_stat.tsx create mode 100644 x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/constants.ts create mode 100644 x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/index_status_stat.tsx create mode 100644 x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/quick_stats_container.tsx create mode 100644 x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/setup_ai_search_button.tsx create mode 100644 x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/stateful_document_count_stat.tsx create mode 100644 x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/stateful_storage_stat.tsx create mode 100644 x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/stateless_document_cout_stat.tsx create mode 100644 x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/stateless_quick_stats.tsx create mode 100644 x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/styles.ts diff --git a/x-pack/solutions/search/plugins/search_indices/public/components/indices/details_page.tsx b/x-pack/solutions/search/plugins/search_indices/public/components/indices/details_page.tsx index 16b355955c2b2..9ebe524b3f903 100644 --- a/x-pack/solutions/search/plugins/search_indices/public/components/indices/details_page.tsx +++ b/x-pack/solutions/search/plugins/search_indices/public/components/indices/details_page.tsx @@ -47,6 +47,7 @@ export const SearchIndexDetailsPage = () => { const tabId = decodeURIComponent(useParams<{ tabId: string }>().tabId); const { + cloud, console: consolePlugin, docLinks, application, @@ -290,7 +291,12 @@ export const SearchIndexDetailsPage = () => { - + diff --git a/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/ai_search_stat.tsx b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/ai_search_stat.tsx new file mode 100644 index 0000000000000..587e9a085a6e8 --- /dev/null +++ b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/ai_search_stat.tsx @@ -0,0 +1,84 @@ +/* + * 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 { useEuiTheme } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { SetupAISearchButton } from './setup_ai_search_button'; +import { VectorFieldTypes } from './mappings_convertor'; +import { QuickStat } from './quick_stat'; + +export interface AISearchQuickStatProps { + mappingStats: VectorFieldTypes; + vectorFieldCount: number; + open: boolean; + setOpen: React.Dispatch>; +} + +export const AISearchQuickStat = ({ + mappingStats, + vectorFieldCount, + open, + setOpen, +}: AISearchQuickStatProps) => { + const { euiTheme } = useEuiTheme(); + return ( + 0 + ? i18n.translate('xpack.searchIndices.quickStats.total_count', { + defaultMessage: '{value, plural, one {# Field} other {# Fields}}', + values: { + value: vectorFieldCount, + }, + }) + : i18n.translate('xpack.searchIndices.quickStats.no_vector_fields', { + defaultMessage: 'Not configured', + }) + } + content={vectorFieldCount === 0 ? : undefined} + stats={[ + { + title: i18n.translate('xpack.searchIndices.quickStats.sparse_vector', { + defaultMessage: 'Sparse Vector', + }), + description: i18n.translate('xpack.searchIndices.quickStats.sparse_vector_count', { + defaultMessage: '{value, plural, one {# Field} other {# Fields}}', + values: { value: mappingStats.sparse_vector }, + }), + }, + { + title: i18n.translate('xpack.searchIndices.quickStats.dense_vector', { + defaultMessage: 'Dense Vector', + }), + description: i18n.translate('xpack.searchIndices.quickStats.dense_vector_count', { + defaultMessage: '{value, plural, one {# Field} other {# Fields}}', + values: { value: mappingStats.dense_vector }, + }), + }, + { + title: i18n.translate('xpack.searchIndices.quickStats.semantic_text', { + defaultMessage: 'Semantic Text', + }), + description: i18n.translate('xpack.searchIndices.quickStats.semantic_text_count', { + defaultMessage: '{value, plural, one {# Field} other {# Fields}}', + values: { value: mappingStats.semantic_text }, + }), + }, + ]} + /> + ); +}; diff --git a/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/aliases_quick_stat.tsx b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/aliases_quick_stat.tsx new file mode 100644 index 0000000000000..3414104b8cb03 --- /dev/null +++ b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/aliases_quick_stat.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiI18nNumber, useEuiTheme } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { QuickStat } from './quick_stat'; +import { AliasesContentStyle } from './styles'; + +export interface AliasesStatProps { + aliases: string[]; + open: boolean; + setOpen: React.Dispatch>; +} + +export const AliasesStat = ({ aliases, open, setOpen }: AliasesStatProps) => { + const { euiTheme } = useEuiTheme(); + return ( + } + content={ + + {aliases.map((alias, i) => ( + + + {alias} + + + ))} + + } + stats={[]} + /> + ); +}; diff --git a/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/constants.ts b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/constants.ts new file mode 100644 index 0000000000000..fca64ceb1ad60 --- /dev/null +++ b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/constants.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const DOCUMENT_COUNT_LABEL = i18n.translate( + 'xpack.searchIndices.quickStats.document_count_heading', + { + defaultMessage: 'Document count', + } +); +export const TOTAL_COUNT_LABEL = i18n.translate( + 'xpack.searchIndices.quickStats.documents.totalTitle', + { + defaultMessage: 'Total', + } +); +export const DELETED_COUNT_LABEL = i18n.translate( + 'xpack.searchIndices.quickStats.documents.deletedTitle', + { + defaultMessage: 'Deleted', + } +); +export const INDEX_SIZE_LABEL = i18n.translate( + 'xpack.searchIndices.quickStats.documents.indexSize', + { + defaultMessage: 'Index Size', + } +); +export const DOCUMENT_COUNT_TOOLTIP = i18n.translate( + 'xpack.searchIndices.quickStats.documentCountTooltip', + { + defaultMessage: + 'This excludes nested documents, which Elasticsearch uses internally to store chunks of vectors.', + } +); diff --git a/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/index_status_stat.tsx b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/index_status_stat.tsx new file mode 100644 index 0000000000000..ef7de78e34caf --- /dev/null +++ b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/index_status_stat.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useMemo } from 'react'; +import { type IndicesStatsIndexMetadataState } from '@elastic/elasticsearch/lib/api/types'; +import type { Index } from '@kbn/index-management-shared-types'; + +import { i18n } from '@kbn/i18n'; + +import { QuickStat, type QuickStatDefinition } from './quick_stat'; +import { + indexHealthToHealthColor, + HealthStatusStrings, + normalizeHealth, +} from '../../utils/indices'; + +export const healthTitleMap: Record = { + red: i18n.translate('xpack.searchIndices.quickStats.indexHealth.red', { defaultMessage: 'Red' }), + green: i18n.translate('xpack.searchIndices.quickStats.indexHealth.green', { + defaultMessage: 'Green', + }), + yellow: i18n.translate('xpack.searchIndices.quickStats.indexHealth.yellow', { + defaultMessage: 'Yellow', + }), + unavailable: i18n.translate('xpack.searchIndices.quickStats.indexHealth.unavailable', { + defaultMessage: 'Unavailable', + }), +}; +export const statusDescriptionMap: Record = { + open: i18n.translate('xpack.searchIndices.quickStats.indexStatus.open', { + defaultMessage: 'Index available', + }), + close: i18n.translate('xpack.searchIndices.quickStats.indexStatus.close', { + defaultMessage: 'Index unavailable', + }), + undefined: i18n.translate('xpack.searchIndices.quickStats.indexStatus.undefined', { + defaultMessage: 'Unknown', + }), +}; + +export interface IndexStatusStatProps { + index: Index; + open: boolean; + setOpen: React.Dispatch>; +} + +function safelyParseShardCount(count: string | number) { + if (typeof count === 'number') return count; + const parsedValue = parseInt(count, 10); + if (!isNaN(parsedValue)) return parsedValue; + return undefined; +} + +export const IndexStatusStat = ({ index, open, setOpen }: IndexStatusStatProps) => { + const { replicaShards, stats: indexStats } = useMemo(() => { + let primaryShardCount: number | undefined; + let replicaShardCount: number | undefined; + const stats: QuickStatDefinition[] = [ + { + title: i18n.translate('xpack.searchIndices.quickStats.indexStatus.title', { + defaultMessage: 'Status', + }), + description: statusDescriptionMap[index.status ?? 'undefined'], + }, + ]; + if (index.primary) { + primaryShardCount = safelyParseShardCount(index.primary); + stats.push({ + title: i18n.translate('xpack.searchIndices.quickStats.indexStatus.primary', { + defaultMessage: 'Primary shards', + }), + description: index.primary, + }); + } + if (index.replica) { + replicaShardCount = safelyParseShardCount(index.replica); + stats.push({ + title: i18n.translate('xpack.searchIndices.quickStats.indexStatus.replica', { + defaultMessage: 'Replica shards', + }), + description: index.replica, + }); + } + + return { stats, primaryShards: primaryShardCount, replicaShards: replicaShardCount }; + }, [index]); + return ( + + ); +}; diff --git a/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/mappings_convertor.ts b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/mappings_convertor.ts index b6063c7c3694c..4683d798b5f29 100644 --- a/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/mappings_convertor.ts +++ b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/mappings_convertor.ts @@ -8,7 +8,7 @@ import type { MappingProperty, MappingPropertyBase } from '@elastic/elasticsearch/lib/api/types'; import type { Mappings } from '../../types'; -interface VectorFieldTypes { +export interface VectorFieldTypes { semantic_text: number; dense_vector: number; sparse_vector: number; diff --git a/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/quick_stat.tsx b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/quick_stat.tsx index 9f2e3785b3681..9067af932bcfa 100644 --- a/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/quick_stat.tsx +++ b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/quick_stat.tsx @@ -6,7 +6,6 @@ */ import React from 'react'; - import { EuiAccordion, EuiDescriptionList, @@ -21,20 +20,22 @@ import { EuiIconTip, } from '@elastic/eui'; -interface BaseQuickStatProps { +export interface BaseQuickStatProps { icon: string; iconColor: string; title: string; - secondaryTitle: React.ReactNode; + secondaryTitle?: React.ReactNode; open: boolean; content?: React.ReactNode; - stats: Array<{ - title: string; - description: NonNullable; - }>; + stats: QuickStatDefinition[]; setOpen: (open: boolean) => void; - first?: boolean; tooltipContent?: string; + statsColumnWidths?: [string | number, string | number] | undefined; +} + +export interface QuickStatDefinition { + title: string; + description: NonNullable; } export const QuickStat: React.FC = ({ @@ -43,11 +44,11 @@ export const QuickStat: React.FC = ({ stats, open, setOpen, - first, secondaryTitle, iconColor, content, tooltipContent, + statsColumnWidths, ...rest }) => { const { euiTheme } = useEuiTheme(); @@ -60,6 +61,7 @@ export const QuickStat: React.FC = ({ return ( setOpen(!open)} paddingSize="none" id={id} @@ -67,8 +69,6 @@ export const QuickStat: React.FC = ({ arrowDisplay="right" {...rest} css={{ - borderLeft: euiTheme.border.thin, - ...(first ? { borderLeftWidth: 0 } : {}), '.euiAccordion__arrow': { marginRight: euiTheme.size.s, }, @@ -76,7 +76,6 @@ export const QuickStat: React.FC = ({ background: euiTheme.colors.emptyShade, }, '.euiAccordion__children': { - borderTop: euiTheme.border.thin, padding: euiTheme.size.m, }, }} @@ -84,21 +83,25 @@ export const QuickStat: React.FC = ({ - + + +

{title}

- - - {secondaryTitle} - - + {secondaryTitle && ( + + + {secondaryTitle} + + + )} {tooltipContent && ( - - + + )}
@@ -113,7 +116,7 @@ export const QuickStat: React.FC = ({ { - const { - services: { docLinks }, - } = useKibana(); - return ( - - - - -
- {i18n.translate('xpack.searchIndices.quickStats.setup_ai_search_description', { - defaultMessage: 'Build AI-powered search experiences with Elastic', - })} -
-
-
- - - {i18n.translate('xpack.searchIndices.quickStats.setup_ai_search_button', { - defaultMessage: 'Set up now', - })} - - -
-
- ); -}; - -export const QuickStats: React.FC = ({ index, mappings, indexDocuments }) => { +export const QuickStats: React.FC = ({ + index, + mappings, + indexDocuments, + isStateless, +}) => { const [open, setOpen] = useState(false); const { euiTheme } = useEuiTheme(); - const mappingStats = useMemo(() => countVectorBasedTypesFromMappings(mappings), [mappings]); - const vectorFieldCount = - mappingStats.sparse_vector + mappingStats.dense_vector + mappingStats.semantic_text; - const docCount = indexDocuments?.results._meta.page.total ?? 0; + const { mappingStats, vectorFieldCount } = useMemo(() => { + const stats = countVectorBasedTypesFromMappings(mappings); + const vectorFields = stats.sparse_vector + stats.dense_vector + stats.semantic_text; + return { mappingStats: stats, vectorFieldCount: vectorFields }; + }, [mappings]); + + const stats = isStateless + ? [ + , + ...(Array.isArray(index.aliases) && index.aliases.length > 0 + ? [] + : []), + , + ] + : [ + , + , + , + ...(Array.isArray(index.aliases) && index.aliases.length > 0 + ? [] + : []), + , + ]; return ( ({ - border: euiTheme.border.thin, - background: euiTheme.colors.lightestShade, - overflow: 'hidden', - })} + css={QuickStatsPanelStyle(euiTheme)} > - - - } - stats={[ - { - title: i18n.translate('xpack.searchIndices.quickStats.documents.totalTitle', { - defaultMessage: 'Total', - }), - description: , - }, - { - title: i18n.translate('xpack.searchIndices.quickStats.documents.indexSize', { - defaultMessage: 'Index Size', - }), - description: index.size ?? '0b', - }, - ]} - tooltipContent={i18n.translate('xpack.searchIndices.quickStats.documentCountTooltip', { - defaultMessage: - 'This excludes nested documents, which Elasticsearch uses internally to store chunks of vectors.', - })} - first - /> - - - 0 - ? i18n.translate('xpack.searchIndices.quickStats.total_count', { - defaultMessage: '{value, plural, one {# Field} other {# Fields}}', - values: { - value: vectorFieldCount, - }, - }) - : i18n.translate('xpack.searchIndices.quickStats.no_vector_fields', { - defaultMessage: 'Not configured', - }) - } - content={vectorFieldCount === 0 && } - stats={[ - { - title: i18n.translate('xpack.searchIndices.quickStats.sparse_vector', { - defaultMessage: 'Sparse Vector', - }), - description: i18n.translate('xpack.searchIndices.quickStats.sparse_vector_count', { - defaultMessage: '{value, plural, one {# Field} other {# Fields}}', - values: { value: mappingStats.sparse_vector }, - }), - }, - { - title: i18n.translate('xpack.searchIndices.quickStats.dense_vector', { - defaultMessage: 'Dense Vector', - }), - description: i18n.translate('xpack.searchIndices.quickStats.dense_vector_count', { - defaultMessage: '{value, plural, one {# Field} other {# Fields}}', - values: { value: mappingStats.dense_vector }, - }), - }, - { - title: i18n.translate('xpack.searchIndices.quickStats.semantic_text', { - defaultMessage: 'Semantic Text', - }), - description: i18n.translate('xpack.searchIndices.quickStats.semantic_text_count', { - defaultMessage: '{value, plural, one {# Field} other {# Fields}}', - values: { value: mappingStats.semantic_text }, - }), - }, - ]} - /> - - + {isStateless ? ( + {stats} + ) : ( + {stats} + )} ); }; diff --git a/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/quick_stats_container.tsx b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/quick_stats_container.tsx new file mode 100644 index 0000000000000..6601fdffe9d8c --- /dev/null +++ b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/quick_stats_container.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiFlexItem, useEuiTheme } from '@elastic/eui'; +import { StatsGridContainerStyle, StatsItemStyle } from './styles'; + +export const QuickStatsContainer = ({ children }: { children: React.ReactNode[] }) => { + const { euiTheme } = useEuiTheme(); + return ( +
+ {children.map((item, i) => + item ? ( + + {item} + + ) : null + )} +
+ ); +}; diff --git a/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/setup_ai_search_button.tsx b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/setup_ai_search_button.tsx new file mode 100644 index 0000000000000..68d23d74fb8bd --- /dev/null +++ b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/setup_ai_search_button.tsx @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText, EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { useKibana } from '../../hooks/use_kibana'; + +export const SetupAISearchButton: React.FC = () => { + const { + services: { docLinks }, + } = useKibana(); + return ( + + + + +
+ {i18n.translate('xpack.searchIndices.quickStats.setup_ai_search_description', { + defaultMessage: 'Build AI-powered search experiences with Elastic', + })} +
+
+
+ + + {i18n.translate('xpack.searchIndices.quickStats.setup_ai_search_button', { + defaultMessage: 'Set up now', + })} + + +
+
+ ); +}; diff --git a/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/stateful_document_count_stat.tsx b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/stateful_document_count_stat.tsx new file mode 100644 index 0000000000000..cf29b9db895e4 --- /dev/null +++ b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/stateful_document_count_stat.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 type { Index } from '@kbn/index-management-shared-types'; + +import { EuiI18nNumber, useEuiTheme } from '@elastic/eui'; + +import { QuickStat } from './quick_stat'; +import { + DELETED_COUNT_LABEL, + DOCUMENT_COUNT_LABEL, + DOCUMENT_COUNT_TOOLTIP, + TOTAL_COUNT_LABEL, +} from './constants'; +import { VectorFieldTypes } from './mappings_convertor'; + +export interface StatefulDocumentCountStatProps { + index: Index; + mappingStats: VectorFieldTypes; + open: boolean; + setOpen: React.Dispatch>; +} + +export const StatefulDocumentCountStat = ({ + index, + open, + setOpen, + mappingStats, +}: StatefulDocumentCountStatProps) => { + const { euiTheme } = useEuiTheme(); + return ( + } + stats={[ + { + title: TOTAL_COUNT_LABEL, + description: , + }, + { + title: DELETED_COUNT_LABEL, + description: , + }, + ]} + tooltipContent={mappingStats.semantic_text > 0 ? DOCUMENT_COUNT_TOOLTIP : undefined} + /> + ); +}; diff --git a/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/stateful_storage_stat.tsx b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/stateful_storage_stat.tsx new file mode 100644 index 0000000000000..dea0d05ce8b17 --- /dev/null +++ b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/stateful_storage_stat.tsx @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Index } from '@kbn/index-management-shared-types'; + +import { useEuiTheme } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { QuickStat } from './quick_stat'; +import { INDEX_SIZE_LABEL } from './constants'; + +export interface StatefulIndexStorageStatProps { + index: Index; + open: boolean; + setOpen: React.Dispatch>; +} + +export const StatefulIndexStorageStat = ({ + index, + open, + setOpen, +}: StatefulIndexStorageStatProps) => { + const { euiTheme } = useEuiTheme(); + return ( + + ); +}; diff --git a/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/stateless_document_cout_stat.tsx b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/stateless_document_cout_stat.tsx new file mode 100644 index 0000000000000..3b6ecee0576a0 --- /dev/null +++ b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/stateless_document_cout_stat.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import type { Index } from '@kbn/index-management-shared-types'; + +import { EuiI18nNumber, useEuiTheme } from '@elastic/eui'; + +import { QuickStat } from './quick_stat'; +import { + DOCUMENT_COUNT_LABEL, + DOCUMENT_COUNT_TOOLTIP, + INDEX_SIZE_LABEL, + TOTAL_COUNT_LABEL, +} from './constants'; + +export interface StatelessDocumentCountStatProps { + index: Index; + documentCount: number; + open: boolean; + setOpen: React.Dispatch>; +} + +export const StatelessDocumentCountStat = ({ + index, + documentCount, + open, + setOpen, +}: StatelessDocumentCountStatProps) => { + const { euiTheme } = useEuiTheme(); + return ( + } + stats={[ + { + title: TOTAL_COUNT_LABEL, + description: , + }, + { + title: INDEX_SIZE_LABEL, + description: index.size ?? '0b', + }, + ]} + tooltipContent={DOCUMENT_COUNT_TOOLTIP} + /> + ); +}; diff --git a/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/stateless_quick_stats.tsx b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/stateless_quick_stats.tsx new file mode 100644 index 0000000000000..35e8d68d8a333 --- /dev/null +++ b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/stateless_quick_stats.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui'; + +import { StatsItemStyle } from './styles'; + +export interface StatelessQuickStatsProps { + children: React.ReactNode[]; +} + +export const StatelessQuickStats = ({ children }: StatelessQuickStatsProps) => { + const { euiTheme } = useEuiTheme(); + return ( + + {children.map((stat, i) => ( + + {stat} + + ))} + + ); +}; diff --git a/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/styles.ts b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/styles.ts new file mode 100644 index 0000000000000..c08f5d594cdf7 --- /dev/null +++ b/x-pack/solutions/search/plugins/search_indices/public/components/quick_stats/styles.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { css } from '@emotion/react'; + +import { type UseEuiTheme } from '@elastic/eui'; + +export const QuickStatsPanelStyle = (euiTheme: UseEuiTheme['euiTheme']) => css` + background: ${euiTheme.colors.lightestShade}; + overflow: hidden; +`; + +export const StatsGridContainerStyle = css` + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); +`; + +export const StatsItemStyle = (euiTheme: UseEuiTheme['euiTheme']) => css` + border: ${euiTheme.border.thin}; + min-width: 250px; +`; + +export const AliasesContentStyle = css` + max-height: 100px; +`; diff --git a/x-pack/solutions/search/plugins/search_indices/public/utils/indices.ts b/x-pack/solutions/search/plugins/search_indices/public/utils/indices.ts index 3812eea8757b9..ff2214c660432 100644 --- a/x-pack/solutions/search/plugins/search_indices/public/utils/indices.ts +++ b/x-pack/solutions/search/plugins/search_indices/public/utils/indices.ts @@ -5,6 +5,10 @@ * 2.0. */ +import type { HealthStatus } from '@elastic/elasticsearch/lib/api/types'; + +import type { IconColor } from '@elastic/eui'; + // see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html for the current rules export function isValidIndexName(name: string) { @@ -44,3 +48,19 @@ export function getFirstNewIndexName(startingIndexNames: string[], currentIndexN } return undefined; } + +export type HealthStatusStrings = 'red' | 'green' | 'yellow' | 'unavailable'; +export const healthColorsMap: Record = { + red: 'danger', + green: 'success', + yellow: 'warning', + unavailable: '', +}; + +export const normalizeHealth = (health: HealthStatusStrings | HealthStatus): HealthStatusStrings => + health.toLowerCase() as HealthStatusStrings; +export const indexHealthToHealthColor = ( + health: HealthStatus | 'unavailable' = 'unavailable' +): IconColor => { + return healthColorsMap[normalizeHealth(health)] ?? healthColorsMap.unavailable; +}; diff --git a/x-pack/test/functional/page_objects/search_index_details_page.ts b/x-pack/test/functional/page_objects/search_index_details_page.ts index c6afabfa25569..61e9b4e031262 100644 --- a/x-pack/test/functional/page_objects/search_index_details_page.ts +++ b/x-pack/test/functional/page_objects/search_index_details_page.ts @@ -50,12 +50,26 @@ export function SearchIndexDetailPageProvider({ getService }: FtrProviderContext 'QuickStatsDocumentCount' ); expect(await quickStatsDocumentElem.getVisibleText()).to.contain('Document count\n0'); - expect(await quickStatsDocumentElem.getVisibleText()).not.to.contain('Index Size\n0b'); + expect(await quickStatsDocumentElem.getVisibleText()).not.to.contain('Total\n0'); await quickStatsDocumentElem.click(); - expect(await quickStatsDocumentElem.getVisibleText()).to.contain('Index Size\n227b'); + expect(await quickStatsDocumentElem.getVisibleText()).to.contain('Total\n0\nDeleted\n0'); + }, + + async expectQuickStatsToHaveIndexStatus() { + await testSubjects.existOrFail('QuickStatsIndexStatus'); + }, + + async expectQuickStatsToHaveIndexStorage(size?: string) { + await testSubjects.existOrFail('QuickStatsStorage'); + if (!size) return; + + const quickStatsElem = await testSubjects.find('quickStats'); + const quickStatsStorageElem = await quickStatsElem.findByTestSubject('QuickStatsStorage'); + expect(await quickStatsStorageElem.getVisibleText()).to.contain(`Storage\n${size}`); }, async expectQuickStatsToHaveDocumentCount(count: number) { + await testSubjects.existOrFail('QuickStatsDocumentCount'); const quickStatsElem = await testSubjects.find('quickStats'); const quickStatsDocumentElem = await quickStatsElem.findByTestSubject( 'QuickStatsDocumentCount' @@ -65,6 +79,7 @@ export function SearchIndexDetailPageProvider({ getService }: FtrProviderContext async expectQuickStatsAIMappings() { await testSubjects.existOrFail('quickStats', { timeout: 2000 }); + await testSubjects.existOrFail('QuickStatsAIMappings'); const quickStatsElem = await testSubjects.find('quickStats'); const quickStatsAIMappingsElem = await quickStatsElem.findByTestSubject( 'QuickStatsAIMappings' diff --git a/x-pack/test/functional_search/tests/search_index_details.ts b/x-pack/test/functional_search/tests/search_index_details.ts index 2efeccff5c667..0dd464cc5864e 100644 --- a/x-pack/test/functional_search/tests/search_index_details.ts +++ b/x-pack/test/functional_search/tests/search_index_details.ts @@ -112,6 +112,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('should have quick stats', async () => { await pageObjects.searchIndexDetailsPage.expectQuickStats(); + await pageObjects.searchIndexDetailsPage.expectQuickStatsToHaveIndexStatus(); + await pageObjects.searchIndexDetailsPage.expectQuickStatsToHaveIndexStorage('227b'); await pageObjects.searchIndexDetailsPage.expectQuickStatsAIMappings(); await es.indices.putMapping({ index: indexName, @@ -187,6 +189,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('should be able to delete document', async () => { await pageObjects.searchIndexDetailsPage.changeTab('dataTab'); await pageObjects.searchIndexDetailsPage.clickFirstDocumentDeleteAction(); + + // re-open page to refresh queries for test (these will auto-refresh, + // but waiting for that will make this test flakey) + await pageObjects.searchNavigation.navigateToIndexDetailPage(indexName); await pageObjects.searchIndexDetailsPage.expectAddDocumentCodeExamples(); await pageObjects.searchIndexDetailsPage.expectQuickStatsToHaveDocumentCount(0); }); From 5b22aa9b66a1d569f65ef0f485290d15a804353f Mon Sep 17 00:00:00 2001 From: Tiago Vila Verde Date: Wed, 29 Jan 2025 21:31:47 +0100 Subject: [PATCH 10/12] [Entity Analytics][Entity Store] Add transform config options to the API (#208062) ## Summary This PR adds the following parameters to the `INIT` engine API: * `frequency`: the transform run frequency * `timeout`: the timeout for the initial creation of the transform * `docsPerSecond`: transform throttling option. See [here](https://arc.net/l/quote/vxcmfnhh) * `delay`: The transform delay duration. See [here](https://arc.net/l/quote/mzvaexhv) Coming soon In addition, the PR adds these fields to the Saved Object with the engine descriptor, as well as providing a migration with the appropriate backfilling. Finally, there are some utility function that were/are helpful in working with objects. ## How to test *NOTE*: Always make sure the security default data view exists. Easiest way it to just navigate to some Security UI. ### Checking the new defaults 1. Initialize an engine via dev tools by calling: `POST kbn:/api/entity_store/engines//init {}` 2. Call `GET kbn:/api/entity_store/status`. This response should now contain all the default optional values. ### Observing the parameters are being applied 1. Initialize an engine via the API. This time pass any of the `timeout, frequency, delay and docsPerSecond` options in the request body. 2. Once the `status` changes to `started`, query the respective transform: `GET _transform/entities-v1-latest-security__default` 3. Check that the parameters have been applied to the transform ### Checking Saved Object Migration 1. Check out `main`. 2. Initialize the store. 3. Query `GET kbn:/api/entity_store/status`. Note down the fields in the engine object. 4. Check out this branch. 5. Restart kibana. 6. Query `GET kbn:/api/entity_store/status` again. Observe the new fields have been added and backfilled --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- oas_docs/output/kibana.serverless.yaml | 57 +- oas_docs/output/kibana.yaml | 57 +- .../check_registered_types.test.ts | 2 +- .../src/schema/entity_definition.ts | 2 + .../generate_latest_transform.test.ts.snap | 2 + .../transform/generate_latest_transform.ts | 7 +- .../entity_store/common.gen.ts | 16 + .../entity_store/common.schema.yaml | 14 + .../entity_store/enable.gen.ts | 38 +- .../entity_store/enable.schema.yaml | 28 +- .../entity_store/engine/init.gen.ts | 36 + .../entity_store/engine/init.schema.yaml | 26 + .../common/utils/objects/get.ts | 32 + .../common/utils/objects/merge.test.ts | 28 + .../common/utils/objects/merge.ts | 30 + .../common/utils/objects/modify.ts | 41 ++ .../common/utils/objects/set.ts | 71 ++ .../common/utils/objects/types.ts | 12 + .../common/utils/objects/update.test.ts | 22 + .../common/utils/objects/update.ts | 41 ++ ...alytics_api_2023_10_31.bundled.schema.yaml | 61 +- ...alytics_api_2023_10_31.bundled.schema.yaml | 61 +- .../engine_status_header_action.test.tsx | 4 +- .../entity_store/constants.ts | 31 +- .../elasticsearch_assets/entity_index.ts | 2 +- .../entity_manager_conversion.ts | 2 + .../entity_store/entity_definitions/types.ts | 2 +- .../entity_store_data_client.test.ts | 7 +- .../entity_store/entity_store_data_client.ts | 96 +-- .../engine_description.test.ts.snap | 662 +++++++++++++++++ .../installation/engine_description.test.ts | 668 +----------------- .../installation/engine_description.ts | 94 ++- .../entity_store/installation/types.ts | 4 +- .../saved_object/engine_descriptor.ts | 27 +- .../saved_object/engine_descriptor_type.ts | 19 +- .../entity_store.ts | 24 +- .../entity_store_nondefault_spaces.ts | 23 +- 37 files changed, 1490 insertions(+), 859 deletions(-) create mode 100644 x-pack/solutions/security/plugins/security_solution/common/utils/objects/get.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/common/utils/objects/merge.test.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/common/utils/objects/merge.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/common/utils/objects/modify.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/common/utils/objects/set.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/common/utils/objects/types.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/common/utils/objects/update.test.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/common/utils/objects/update.ts create mode 100644 x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/installation/__snapshots__/engine_description.test.ts.snap diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 5af7369ea7c4c..628b514244c51 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -9789,6 +9789,14 @@ paths: schema: type: object properties: + delay: + default: 1m + description: The delay before the transform will run. + pattern: '[smdh]$' + type: string + docsPerSecond: + description: The number of documents per second to process. + type: integer enrichPolicyExecutionInterval: $ref: '#/components/schemas/Security_Entity_Analytics_API_Interval' entityTypes: @@ -9801,11 +9809,21 @@ paths: type: integer filter: type: string + frequency: + default: 1m + description: The frequency at which the transform will run. + pattern: '[smdh]$' + type: string indexPattern: $ref: '#/components/schemas/Security_Entity_Analytics_API_IndexPattern' lookbackPeriod: default: 24h - description: The lookback period for the entity store + description: The amount of time the transform looks back to calculate the aggregations. + pattern: '[smdh]$' + type: string + timeout: + default: 180s + description: The timeout for initializing the aggregating transform. pattern: '[smdh]$' type: string description: Schema for the entity store initialization @@ -9915,6 +9933,14 @@ paths: schema: type: object properties: + delay: + default: 1m + description: The delay before the transform will run. + pattern: '[smdh]$' + type: string + docsPerSecond: + description: The number of documents per second to process. + type: integer enrichPolicyExecutionInterval: $ref: '#/components/schemas/Security_Entity_Analytics_API_Interval' fieldHistoryLength: @@ -9923,8 +9949,23 @@ paths: type: integer filter: type: string + frequency: + default: 1m + description: The frequency at which the transform will run. + pattern: '[smdh]$' + type: string indexPattern: $ref: '#/components/schemas/Security_Entity_Analytics_API_IndexPattern' + lookbackPeriod: + default: 24h + description: The amount of time the transform looks back to calculate the aggregations. + pattern: '[smdh]$' + type: string + timeout: + default: 180s + description: The timeout for initializing the aggregating transform. + pattern: '[smdh]$' + type: string description: Schema for the engine initialization required: true responses: @@ -51361,12 +51402,22 @@ components: Security_Entity_Analytics_API_EngineDescriptor: type: object properties: + delay: + default: 1m + pattern: '[smdh]$' + type: string + docsPerSecond: + type: integer error: type: object fieldHistoryLength: type: integer filter: type: string + frequency: + default: 1m + pattern: '[smdh]$' + type: string indexPattern: $ref: '#/components/schemas/Security_Entity_Analytics_API_IndexPattern' lookbackPeriod: @@ -51375,6 +51426,10 @@ components: type: string status: $ref: '#/components/schemas/Security_Entity_Analytics_API_EngineStatus' + timeout: + default: 180s + pattern: '[smdh]$' + type: string type: $ref: '#/components/schemas/Security_Entity_Analytics_API_EntityType' required: diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 4ab7baa851bfa..e3f13a8dcd868 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -11879,6 +11879,14 @@ paths: schema: type: object properties: + delay: + default: 1m + description: The delay before the transform will run. + pattern: '[smdh]$' + type: string + docsPerSecond: + description: The number of documents per second to process. + type: integer enrichPolicyExecutionInterval: $ref: '#/components/schemas/Security_Entity_Analytics_API_Interval' entityTypes: @@ -11891,11 +11899,21 @@ paths: type: integer filter: type: string + frequency: + default: 1m + description: The frequency at which the transform will run. + pattern: '[smdh]$' + type: string indexPattern: $ref: '#/components/schemas/Security_Entity_Analytics_API_IndexPattern' lookbackPeriod: default: 24h - description: The lookback period for the entity store + description: The amount of time the transform looks back to calculate the aggregations. + pattern: '[smdh]$' + type: string + timeout: + default: 180s + description: The timeout for initializing the aggregating transform. pattern: '[smdh]$' type: string description: Schema for the entity store initialization @@ -12001,6 +12019,14 @@ paths: schema: type: object properties: + delay: + default: 1m + description: The delay before the transform will run. + pattern: '[smdh]$' + type: string + docsPerSecond: + description: The number of documents per second to process. + type: integer enrichPolicyExecutionInterval: $ref: '#/components/schemas/Security_Entity_Analytics_API_Interval' fieldHistoryLength: @@ -12009,8 +12035,23 @@ paths: type: integer filter: type: string + frequency: + default: 1m + description: The frequency at which the transform will run. + pattern: '[smdh]$' + type: string indexPattern: $ref: '#/components/schemas/Security_Entity_Analytics_API_IndexPattern' + lookbackPeriod: + default: 24h + description: The amount of time the transform looks back to calculate the aggregations. + pattern: '[smdh]$' + type: string + timeout: + default: 180s + description: The timeout for initializing the aggregating transform. + pattern: '[smdh]$' + type: string description: Schema for the engine initialization required: true responses: @@ -58051,12 +58092,22 @@ components: Security_Entity_Analytics_API_EngineDescriptor: type: object properties: + delay: + default: 1m + pattern: '[smdh]$' + type: string + docsPerSecond: + type: integer error: type: object fieldHistoryLength: type: integer filter: type: string + frequency: + default: 1m + pattern: '[smdh]$' + type: string indexPattern: $ref: '#/components/schemas/Security_Entity_Analytics_API_IndexPattern' lookbackPeriod: @@ -58065,6 +58116,10 @@ components: type: string status: $ref: '#/components/schemas/Security_Entity_Analytics_API_EngineStatus' + timeout: + default: 180s + pattern: '[smdh]$' + type: string type: $ref: '#/components/schemas/Security_Entity_Analytics_API_EntityType' required: diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts index 91a318edd99b2..90973a28ba9ab 100644 --- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts +++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts @@ -97,7 +97,7 @@ describe('checking migration metadata changes on all registered SO types', () => "enterprise_search_telemetry": "9ac912e1417fc8681e0cd383775382117c9e3d3d", "entity-definition": "1c6bff35c423d5dc5650bc806cf2899e4706a0bc", "entity-discovery-api-key": "c267a65c69171d1804362155c1378365f5acef88", - "entity-engine-status": "8cb7dcb13f5e2ea8f2e08dd4af72c110e2051120", + "entity-engine-status": "e2de87d84e9f1f72726eb28b7e670ff8021b5eb4", "epm-packages": "8042d4a1522f6c4e6f5486e791b3ffe3a22f88fd", "epm-packages-assets": "7a3e58efd9a14191d0d1a00b8aaed30a145fd0b1", "event-annotation-group": "715ba867d8c68f3c9438052210ea1c30a9362582", diff --git a/x-pack/platform/packages/shared/kbn-entities-schema/src/schema/entity_definition.ts b/x-pack/platform/packages/shared/kbn-entities-schema/src/schema/entity_definition.ts index d9d8e6b610013..c9a900f0f2091 100644 --- a/x-pack/platform/packages/shared/kbn-entities-schema/src/schema/entity_definition.ts +++ b/x-pack/platform/packages/shared/kbn-entities-schema/src/schema/entity_definition.ts @@ -38,6 +38,8 @@ export const entityDefinitionSchema = z.object({ syncField: z.optional(z.string()), syncDelay: z.optional(durationSchema), frequency: z.optional(durationSchema), + timeout: z.optional(durationSchema), + docsPerSecond: z.optional(z.number()), }) ), }), diff --git a/x-pack/platform/plugins/shared/entity_manager/server/lib/entities/transform/__snapshots__/generate_latest_transform.test.ts.snap b/x-pack/platform/plugins/shared/entity_manager/server/lib/entities/transform/__snapshots__/generate_latest_transform.test.ts.snap index 86978b1b4df95..ee7ae4d1247ba 100644 --- a/x-pack/platform/plugins/shared/entity_manager/server/lib/entities/transform/__snapshots__/generate_latest_transform.test.ts.snap +++ b/x-pack/platform/plugins/shared/entity_manager/server/lib/entities/transform/__snapshots__/generate_latest_transform.test.ts.snap @@ -148,6 +148,7 @@ Object { }, "settings": Object { "deduce_mappings": false, + "docs_per_second": undefined, "unattended": true, }, "source": Object { @@ -179,6 +180,7 @@ Object { "field": "@timestamp", }, }, + "timeout": undefined, "transform_id": "entities-v1-latest-admin-console-services", } `; diff --git a/x-pack/platform/plugins/shared/entity_manager/server/lib/entities/transform/generate_latest_transform.ts b/x-pack/platform/plugins/shared/entity_manager/server/lib/entities/transform/generate_latest_transform.ts index c9d8cd9deef9b..06e92a7ddc6e7 100644 --- a/x-pack/platform/plugins/shared/entity_manager/server/lib/entities/transform/generate_latest_transform.ts +++ b/x-pack/platform/plugins/shared/entity_manager/server/lib/entities/transform/generate_latest_transform.ts @@ -50,6 +50,7 @@ export function generateLatestTransform( transformId: generateLatestTransformId(definition), frequency: definition.latest.settings?.frequency ?? ENTITY_DEFAULT_LATEST_FREQUENCY, syncDelay: definition.latest.settings?.syncDelay ?? ENTITY_DEFAULT_LATEST_SYNC_DELAY, + docsPerSecond: definition.latest.settings?.docsPerSecond, }); } @@ -59,13 +60,15 @@ const generateTransformPutRequest = ({ transformId, frequency, syncDelay, + docsPerSecond, }: { definition: EntityDefinition; transformId: string; filter: QueryDslQueryContainer[]; frequency: string; syncDelay: string; -}) => { + docsPerSecond?: number; +}): TransformPutTransformRequest => { return { transform_id: transformId, _meta: { @@ -73,6 +76,7 @@ const generateTransformPutRequest = ({ managed: definition.managed, }, defer_validation: true, + timeout: definition.latest.settings?.timeout, source: { index: definition.indexPatterns, ...(filter.length > 0 && { @@ -97,6 +101,7 @@ const generateTransformPutRequest = ({ settings: { deduce_mappings: false, unattended: true, + docs_per_second: docsPerSecond, }, pivot: { group_by: { diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/common.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/common.gen.ts index 3c809aaac73ee..f6af2074fca99 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/common.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/common.gen.ts @@ -41,6 +41,22 @@ export const EngineDescriptor = z.object({ .regex(/[smdh]$/) .optional() .default('24h'), + timeout: z + .string() + .regex(/[smdh]$/) + .optional() + .default('180s'), + frequency: z + .string() + .regex(/[smdh]$/) + .optional() + .default('1m'), + delay: z + .string() + .regex(/[smdh]$/) + .optional() + .default('1m'), + docsPerSecond: z.number().int().optional(), error: z.object({}).optional(), }); diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/common.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/common.schema.yaml index 1bc6e6b941fe1..e885fd9a1cbf6 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/common.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/common.schema.yaml @@ -36,6 +36,20 @@ components: type: string default: 24h pattern: '[smdh]$' + timeout: + type: string + default: 180s + pattern: '[smdh]$' + frequency: + type: string + default: 1m + pattern: '[smdh]$' + delay: + type: string + default: 1m + pattern: '[smdh]$' + docsPerSecond: + type: integer error: type: object diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/enable.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/enable.gen.ts index 35d7d9d136283..cb1f5743d2ba7 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/enable.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/enable.gen.ts @@ -24,18 +24,46 @@ export const InitEntityStoreRequestBody = z.object({ * The number of historical values to keep for each field. */ fieldHistoryLength: z.number().int().optional().default(10), + indexPattern: IndexPattern.optional(), + filter: z.string().optional(), + entityTypes: z.array(EntityType).optional(), + enrichPolicyExecutionInterval: Interval.optional(), /** - * The lookback period for the entity store + * The amount of time the transform looks back to calculate the aggregations. */ lookbackPeriod: z .string() .regex(/[smdh]$/) .optional() .default('24h'), - indexPattern: IndexPattern.optional(), - filter: z.string().optional(), - entityTypes: z.array(EntityType).optional(), - enrichPolicyExecutionInterval: Interval.optional(), + /** + * The timeout for initializing the aggregating transform. + */ + timeout: z + .string() + .regex(/[smdh]$/) + .optional() + .default('180s'), + /** + * The frequency at which the transform will run. + */ + frequency: z + .string() + .regex(/[smdh]$/) + .optional() + .default('1m'), + /** + * The delay before the transform will run. + */ + delay: z + .string() + .regex(/[smdh]$/) + .optional() + .default('1m'), + /** + * The number of documents per second to process. + */ + docsPerSecond: z.number().int().optional(), }); export type InitEntityStoreRequestBodyInput = z.input; diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/enable.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/enable.schema.yaml index ceb160120cf69..20ab113537ebb 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/enable.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/enable.schema.yaml @@ -23,11 +23,6 @@ paths: type: integer description: The number of historical values to keep for each field. default: 10 - lookbackPeriod: - type: string - description: The lookback period for the entity store - default: 24h - pattern: '[smdh]$' indexPattern: $ref: './common.schema.yaml#/components/schemas/IndexPattern' filter: @@ -38,6 +33,29 @@ paths: $ref: './common.schema.yaml#/components/schemas/EntityType' enrichPolicyExecutionInterval: $ref: './common.schema.yaml#/components/schemas/Interval' + lookbackPeriod: + type: string + default: 24h + pattern: '[smdh]$' + description: The amount of time the transform looks back to calculate the aggregations. + timeout: + type: string + default: 180s + pattern: '[smdh]$' + description: The timeout for initializing the aggregating transform. + frequency: + type: string + default: 1m + pattern: '[smdh]$' + description: The frequency at which the transform will run. + delay: + type: string + default: 1m + pattern: '[smdh]$' + description: The delay before the transform will run. + docsPerSecond: + type: integer + description: The number of documents per second to process. responses: '200': description: Successful response diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/engine/init.gen.ts b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/engine/init.gen.ts index c5d0f438be63d..4833e32e0dcbd 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/engine/init.gen.ts +++ b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/engine/init.gen.ts @@ -36,6 +36,42 @@ export const InitEntityEngineRequestBody = z.object({ indexPattern: IndexPattern.optional(), filter: z.string().optional(), enrichPolicyExecutionInterval: Interval.optional(), + /** + * The amount of time the transform looks back to calculate the aggregations. + */ + lookbackPeriod: z + .string() + .regex(/[smdh]$/) + .optional() + .default('24h'), + /** + * The timeout for initializing the aggregating transform. + */ + timeout: z + .string() + .regex(/[smdh]$/) + .optional() + .default('180s'), + /** + * The frequency at which the transform will run. + */ + frequency: z + .string() + .regex(/[smdh]$/) + .optional() + .default('1m'), + /** + * The delay before the transform will run. + */ + delay: z + .string() + .regex(/[smdh]$/) + .optional() + .default('1m'), + /** + * The number of documents per second to process. + */ + docsPerSecond: z.number().int().optional(), }); export type InitEntityEngineRequestBodyInput = z.input; diff --git a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/engine/init.schema.yaml b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/engine/init.schema.yaml index 155b8bb1e2185..f4af061287d80 100644 --- a/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/engine/init.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/common/api/entity_analytics/entity_store/engine/init.schema.yaml @@ -35,6 +35,32 @@ paths: type: string enrichPolicyExecutionInterval: $ref: '../common.schema.yaml#/components/schemas/Interval' + + lookbackPeriod: + type: string + default: 24h + pattern: '[smdh]$' + description: The amount of time the transform looks back to calculate the aggregations. + timeout: + type: string + default: 180s + pattern: '[smdh]$' + description: The timeout for initializing the aggregating transform. + frequency: + type: string + default: 1m + pattern: '[smdh]$' + description: The frequency at which the transform will run. + delay: + type: string + default: 1m + pattern: '[smdh]$' + description: The delay before the transform will run. + docsPerSecond: + type: integer + description: The number of documents per second to process. + + responses: '200': description: Successful response diff --git a/x-pack/solutions/security/plugins/security_solution/common/utils/objects/get.ts b/x-pack/solutions/security/plugins/security_solution/common/utils/objects/get.ts new file mode 100644 index 0000000000000..a3c01e5b3ac2d --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/common/utils/objects/get.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import fp from 'lodash/fp'; +import type { Get } from 'type-fest'; + +type Path = string | readonly string[]; + +/** Proxy for lodash fp `get` with better type inference. + * Overloaded to support both imperative and point free style + * + * Dynamic paths are supported if an array is passed as `path`. + * If an array is passed as `path`, it needs to be `const`: `["foo", "bar"] as const` + **/ +function get(obj: T, path: P): Get; + +function get(path: P): (obj: T) => Get; + +function get(...args: [T, P] | [P]) { + if (args.length === 2) { + const [obj, path] = args; + return fp.get(path)(obj); + } + const [path] = args; + return fp.get(path); +} + +export { get }; diff --git a/x-pack/solutions/security/plugins/security_solution/common/utils/objects/merge.test.ts b/x-pack/solutions/security/plugins/security_solution/common/utils/objects/merge.test.ts new file mode 100644 index 0000000000000..ee893bee6c3d3 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/common/utils/objects/merge.test.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { merge } from './merge'; + +describe('merge', () => { + it('merges two objects', () => { + const target = { a: 1 }; + const source = { b: 2 }; + expect(merge(target, source)).toEqual({ a: 1, b: 2 }); + }); + + it('merges two objects in point free style', () => { + const target = { a: 1 }; + const source = { b: 2 }; + expect(merge(source)(target)).toEqual({ a: 1, b: 2 }); + }); + + it('overwrites target properties with source properties', () => { + const target = { a: 1, b: 2 }; + const source = { b: 3 }; + expect(merge(target, source)).toEqual({ a: 1, b: 3 }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/common/utils/objects/merge.ts b/x-pack/solutions/security/plugins/security_solution/common/utils/objects/merge.ts new file mode 100644 index 0000000000000..51fa733a150d2 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/common/utils/objects/merge.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import fp from 'lodash/fp'; +import type { Expand } from './types'; + +interface Merge { + (source: Source): ( + target: Target + ) => Expand; + (target: Target, source: Source): Expand< + Target & Source + >; +} + +/** + * Proxy for lodash `merge` with better types + */ +export const merge: Merge = (...args: [S] | [S, T]) => { + if (args.length === 2) { + const [target, source] = args; + return fp.merge(target, source); + } + const [source] = args; + return (target) => fp.merge(target, source); +}; diff --git a/x-pack/solutions/security/plugins/security_solution/common/utils/objects/modify.ts b/x-pack/solutions/security/plugins/security_solution/common/utils/objects/modify.ts new file mode 100644 index 0000000000000..be7b3304bc072 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/common/utils/objects/modify.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 fp from 'lodash/fp'; +import type { Get, IsEqual } from 'type-fest'; +import type { SetProp } from './set'; + +/** + * Proxy for lodash fp `update` with better type inference. + * We use `modify` to signal that the type of the object is being modified. + * Overloaded to support both imperative and point free style. + */ +function modify, B = A>( + path: P, + updater: (val: A) => B +): ( + obj: T +) => IsEqual extends true ? `Could not find path "${P}"` : SetProp; + +function modify, B = A>( + obj: T, + path: P, + updater: (val: A) => B +): IsEqual extends true ? `Could not find path "${P}"` : SetProp; + +function modify, B = A>( + ...args: [P, (val: A) => B] | [T, P, (val: A) => B] +) { + if (args.length === 3) { + const [obj, path, updater] = args; + return fp.update(path)(updater)(obj); + } + const [path, updater] = args; + return fp.update(path)(updater); +} + +export { modify }; diff --git a/x-pack/solutions/security/plugins/security_solution/common/utils/objects/set.ts b/x-pack/solutions/security/plugins/security_solution/common/utils/objects/set.ts new file mode 100644 index 0000000000000..02de840f2885d --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/common/utils/objects/set.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import fp from 'lodash/fp'; + +import type { IsEqual, Get, EmptyObject } from 'type-fest'; +import type { Expand } from './types'; + +type Path = string | readonly string[]; + +/** Proxy for lodash fp `set` with better type inference. Overloaded to support both imperative and point free style. + * + * If an invalid path is passed, the type of the `value` parameter is `never` which should cause a type error. + * Do **not** do an `as never` assertion to get around this, but instead make sure that the path is correct. + * + * Dynamic paths is supported if an array is passed as `path`. + * If an array is passed as `path`, it needs to be `const`: `["foo", "bar"] as const` + */ +function set>( + path: P, + value: IsEqual, unknown> extends true ? never : V +): (obj: T) => IsEqual, unknown> extends true ? unknown : T; + +function set>( + obj: T, + path: P, + value: IsEqual, unknown> extends true ? never : V +): IsEqual, unknown> extends true ? unknown : T; + +function set>(...args: [P, V] | [T, P, V]) { + if (args.length === 3) { + const [obj, path, value] = args; + return fp.set(path)(value)(obj); + } + const [path, value] = args; + return fp.set(path)(value); +} + +export { set }; + +export type SetProp

= Expand< + P extends `${infer K}.${infer Tail}` + ? K extends keyof T + ? { [k in keyof T]: k extends K ? SetProp : T[k] } + : { [k in K]: SetProp } & (T extends EmptyObject ? {} : T) + : P extends keyof T + ? { [k in keyof T]: k extends P ? V : T[k] } + : { [k in P]: V } & (T extends EmptyObject ? {} : T) +>; + +export function setProp( + obj: T, + path: P, + value: V +): SetProp; +export function setProp( + path: P, + value: V +): (obj: T) => SetProp; +export function setProp(...args: [T, P, V] | [P, V]) { + if (args.length === 3) { + const [obj, path, value] = args; + return fp.set(path, value)(obj); + } + const [path, value] = args; + return (obj: T) => fp.set(path, value)(obj); +} diff --git a/x-pack/solutions/security/plugins/security_solution/common/utils/objects/types.ts b/x-pack/solutions/security/plugins/security_solution/common/utils/objects/types.ts new file mode 100644 index 0000000000000..331838c121bf0 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/common/utils/objects/types.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Type utility to force typescript to early evaluate the type. + * This is useful for clarifying type computations + */ +export type Expand = T extends unknown ? { [K in keyof T]: T[K] } : never; diff --git a/x-pack/solutions/security/plugins/security_solution/common/utils/objects/update.test.ts b/x-pack/solutions/security/plugins/security_solution/common/utils/objects/update.test.ts new file mode 100644 index 0000000000000..3abd71745c93a --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/common/utils/objects/update.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { update } from './update'; + +describe('update', () => { + it('updates a nested property', () => { + const obj = { a: { b: 1 } }; + const result = update(obj, 'a.b', (val) => val + 1); + expect(result).toEqual({ a: { b: 2 } }); + }); + + it('updates a nested property in point free style', () => { + const obj = { a: { b: 1 } }; + const result = update('a.b', (val) => val + 1)(obj); + expect(result).toEqual({ a: { b: 2 } }); + }); +}); diff --git a/x-pack/solutions/security/plugins/security_solution/common/utils/objects/update.ts b/x-pack/solutions/security/plugins/security_solution/common/utils/objects/update.ts new file mode 100644 index 0000000000000..992f967fb7cce --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/common/utils/objects/update.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 fp from 'lodash/fp'; +import type { Get, IsEqual } from 'type-fest'; + +type UpdaterFn> = ( + val: IsEqual, unknown> extends true ? never : V +) => IsEqual, unknown> extends true ? never : V; + +/** + * Proxy for lodash fp `update` with better type inference. + * Overloaded to support both imperative and point free style. + */ +function update>( + path: P, + updater: UpdaterFn +): (obj: T) => IsEqual, unknown> extends true ? unknown : T; + +function update>( + obj: T, + path: P, + updater: UpdaterFn +): IsEqual, unknown> extends true ? unknown : T; + +function update>( + ...args: [P, UpdaterFn] | [T, P, UpdaterFn] +) { + if (args.length === 3) { + const [obj, path, updater] = args; + return fp.update(path)(updater)(obj); + } + const [path, updater] = args; + return fp.update(path)(updater); +} + +export { update }; diff --git a/x-pack/solutions/security/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml b/x-pack/solutions/security/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml index 9187185aef5db..1d31e5e1a3330 100644 --- a/x-pack/solutions/security/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/docs/openapi/ess/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml @@ -307,6 +307,14 @@ paths: schema: type: object properties: + delay: + default: 1m + description: The delay before the transform will run. + pattern: '[smdh]$' + type: string + docsPerSecond: + description: The number of documents per second to process. + type: integer enrichPolicyExecutionInterval: $ref: '#/components/schemas/Interval' entityTypes: @@ -319,11 +327,23 @@ paths: type: integer filter: type: string + frequency: + default: 1m + description: The frequency at which the transform will run. + pattern: '[smdh]$' + type: string indexPattern: $ref: '#/components/schemas/IndexPattern' lookbackPeriod: default: 24h - description: The lookback period for the entity store + description: >- + The amount of time the transform looks back to calculate the + aggregations. + pattern: '[smdh]$' + type: string + timeout: + default: 180s + description: The timeout for initializing the aggregating transform. pattern: '[smdh]$' type: string description: Schema for the entity store initialization @@ -429,6 +449,14 @@ paths: schema: type: object properties: + delay: + default: 1m + description: The delay before the transform will run. + pattern: '[smdh]$' + type: string + docsPerSecond: + description: The number of documents per second to process. + type: integer enrichPolicyExecutionInterval: $ref: '#/components/schemas/Interval' fieldHistoryLength: @@ -437,8 +465,25 @@ paths: type: integer filter: type: string + frequency: + default: 1m + description: The frequency at which the transform will run. + pattern: '[smdh]$' + type: string indexPattern: $ref: '#/components/schemas/IndexPattern' + lookbackPeriod: + default: 24h + description: >- + The amount of time the transform looks back to calculate the + aggregations. + pattern: '[smdh]$' + type: string + timeout: + default: 180s + description: The timeout for initializing the aggregating transform. + pattern: '[smdh]$' + type: string description: Schema for the engine initialization required: true responses: @@ -1007,12 +1052,22 @@ components: EngineDescriptor: type: object properties: + delay: + default: 1m + pattern: '[smdh]$' + type: string + docsPerSecond: + type: integer error: type: object fieldHistoryLength: type: integer filter: type: string + frequency: + default: 1m + pattern: '[smdh]$' + type: string indexPattern: $ref: '#/components/schemas/IndexPattern' lookbackPeriod: @@ -1021,6 +1076,10 @@ components: type: string status: $ref: '#/components/schemas/EngineStatus' + timeout: + default: 180s + pattern: '[smdh]$' + type: string type: $ref: '#/components/schemas/EntityType' required: diff --git a/x-pack/solutions/security/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml b/x-pack/solutions/security/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml index 6b4b9276e256a..32d09db129022 100644 --- a/x-pack/solutions/security/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/solutions/security/plugins/security_solution/docs/openapi/serverless/security_solution_entity_analytics_api_2023_10_31.bundled.schema.yaml @@ -307,6 +307,14 @@ paths: schema: type: object properties: + delay: + default: 1m + description: The delay before the transform will run. + pattern: '[smdh]$' + type: string + docsPerSecond: + description: The number of documents per second to process. + type: integer enrichPolicyExecutionInterval: $ref: '#/components/schemas/Interval' entityTypes: @@ -319,11 +327,23 @@ paths: type: integer filter: type: string + frequency: + default: 1m + description: The frequency at which the transform will run. + pattern: '[smdh]$' + type: string indexPattern: $ref: '#/components/schemas/IndexPattern' lookbackPeriod: default: 24h - description: The lookback period for the entity store + description: >- + The amount of time the transform looks back to calculate the + aggregations. + pattern: '[smdh]$' + type: string + timeout: + default: 180s + description: The timeout for initializing the aggregating transform. pattern: '[smdh]$' type: string description: Schema for the entity store initialization @@ -429,6 +449,14 @@ paths: schema: type: object properties: + delay: + default: 1m + description: The delay before the transform will run. + pattern: '[smdh]$' + type: string + docsPerSecond: + description: The number of documents per second to process. + type: integer enrichPolicyExecutionInterval: $ref: '#/components/schemas/Interval' fieldHistoryLength: @@ -437,8 +465,25 @@ paths: type: integer filter: type: string + frequency: + default: 1m + description: The frequency at which the transform will run. + pattern: '[smdh]$' + type: string indexPattern: $ref: '#/components/schemas/IndexPattern' + lookbackPeriod: + default: 24h + description: >- + The amount of time the transform looks back to calculate the + aggregations. + pattern: '[smdh]$' + type: string + timeout: + default: 180s + description: The timeout for initializing the aggregating transform. + pattern: '[smdh]$' + type: string description: Schema for the engine initialization required: true responses: @@ -1007,12 +1052,22 @@ components: EngineDescriptor: type: object properties: + delay: + default: 1m + pattern: '[smdh]$' + type: string + docsPerSecond: + type: integer error: type: object fieldHistoryLength: type: integer filter: type: string + frequency: + default: 1m + pattern: '[smdh]$' + type: string indexPattern: $ref: '#/components/schemas/IndexPattern' lookbackPeriod: @@ -1021,6 +1076,10 @@ components: type: string status: $ref: '#/components/schemas/EngineStatus' + timeout: + default: 180s + pattern: '[smdh]$' + type: string type: $ref: '#/components/schemas/EntityType' required: diff --git a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/engines_status/components/engine_status_header_action.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/engines_status/components/engine_status_header_action.test.tsx index ca67becf64d8a..cebfc2872a162 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/engines_status/components/engine_status_header_action.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/entity_analytics/components/entity_store/components/engines_status/components/engine_status_header_action.test.tsx @@ -14,6 +14,7 @@ import type { GetEntityStoreStatusResponse } from '../../../../../../../common/a import { EntityType } from '../../../../../../../common/entity_analytics/types'; import { TestProviders } from '../../../../../../common/mock'; import type { EngineComponentStatus } from '../../../../../../../common/api/entity_analytics'; +import { defaultOptions } from '../../../../../../../server/lib/entity_analytics/entity_store/constants'; jest.mock('../../../hooks/use_entity_store'); jest.mock('../helpers'); @@ -28,12 +29,11 @@ const defaultComponent: EngineComponentStatus = { }; const defaultEngineResponse: GetEntityStoreStatusResponse['engines'][0] = { + ...defaultOptions, type: EntityType.user, indexPattern: '', status: 'started', - fieldHistoryLength: 0, components: [defaultComponent], - lookbackPeriod: '', }; describe('EngineStatusHeaderAction', () => { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/constants.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/constants.ts index dc455e3006e3d..ae6a823ed344d 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/constants.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/constants.ts @@ -5,9 +5,38 @@ * 2.0. */ -import type { EngineStatus, StoreStatus } from '../../../../common/api/entity_analytics'; +import type { Expand } from '../../../../common/utils/objects/types'; +import type { + EngineStatus, + InitEntityEngineRequestBody, + StoreStatus, +} from '../../../../common/api/entity_analytics'; +import { DEFAULT_INTERVAL } from './tasks/field_retention_enrichment/constants'; export const DEFAULT_LOOKBACK_PERIOD = '24h'; +export const DEFAULT_FIELD_HISTORY_LENGTH = 10; +export const DEFAULT_SYNC_DELAY = '1m'; +export const DEFAULT_TIMEOUT = '180s'; +export const DEFAULT_FREQUENCY = '1m'; +export const DEFAULT_DOCS_PER_SECOND = undefined; +export const DEFAULT_INDEX_PATTERNS = ''; +export const DEFAULT_KQL_FILTER = ''; + +export const defaultOptions: Expand< + Required> & { + docsPerSecond: number | undefined; + } +> = { + delay: DEFAULT_SYNC_DELAY, + timeout: DEFAULT_TIMEOUT, + frequency: DEFAULT_FREQUENCY, + docsPerSecond: DEFAULT_DOCS_PER_SECOND, + lookbackPeriod: DEFAULT_LOOKBACK_PERIOD, + fieldHistoryLength: DEFAULT_FIELD_HISTORY_LENGTH, + indexPattern: DEFAULT_INDEX_PATTERNS, + filter: DEFAULT_KQL_FILTER, + enrichPolicyExecutionInterval: DEFAULT_INTERVAL, +}; export const ENGINE_STATUS: Record, EngineStatus> = { INSTALLING: 'installing', diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/elasticsearch_assets/entity_index.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/elasticsearch_assets/entity_index.ts index 719d2df7a9521..d5aec9eac6f9a 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/elasticsearch_assets/entity_index.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/elasticsearch_assets/entity_index.ts @@ -65,7 +65,7 @@ export const getEntityIndexStatus = async ({ export type MappingProperties = NonNullable; export const generateIndexMappings = ( - description: EntityEngineInstallationDescriptor + description: Pick ): MappingTypeMapping => { const identityFieldMappings: MappingProperties = { [description.identityField]: { diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/entity_manager_conversion.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/entity_manager_conversion.ts index 3c1ba8621051c..ffd5b830dba0b 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/entity_manager_conversion.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/entity_manager_conversion.ts @@ -31,6 +31,8 @@ export const convertToEntityManagerDefinition = ( syncField: description.settings.timestampField, syncDelay: description.settings.syncDelay, frequency: description.settings.frequency, + timeout: description.settings.timeout, + docsPerSecond: description.settings.docsPerSecond, }, }, version: description.version, diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/types.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/types.ts index d7529bcd57f1f..79aed5b273404 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_definitions/types.ts @@ -24,5 +24,5 @@ export type EntityDescription = PickPartial< | 'settings' | 'pipeline' | 'dynamic', - 'indexPatterns' | 'indexMappings' | 'settings' | 'pipeline' | 'dynamic' + 'indexPatterns' | 'indexMappings' | 'settings' | 'dynamic' >; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.test.ts index dd3fcec278f6b..9f6783ccc1df2 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.test.ts @@ -21,6 +21,7 @@ import { convertToEntityManagerDefinition } from './entity_definitions/entity_ma import { EntityType } from '../../../../common/search_strategy'; import type { InitEntityEngineResponse } from '../../../../common/api/entity_analytics'; import type { TaskManagerStartContract } from '@kbn/task-manager-plugin/server'; +import { defaultOptions } from './constants'; const definition: EntityDefinition = convertToEntityManagerDefinition( { @@ -33,6 +34,8 @@ const definition: EntityDefinition = convertToEntityManagerDefinition( indexMappings: {}, indexPatterns: [], settings: { + timeout: '180s', + docsPerSecond: undefined, syncDelay: '1m', frequency: '1m', timestampField: '@timestamp', @@ -354,8 +357,8 @@ describe('EntityStoreDataClient', () => { it('only enable engine for the given entityType', async () => { await dataClient.enable({ + ...defaultOptions, entityTypes: [EntityType.host], - fieldHistoryLength: 1, }); expect(spyInit).toHaveBeenCalledWith(EntityType.host, expect.anything(), expect.anything()); @@ -363,8 +366,8 @@ describe('EntityStoreDataClient', () => { it('does not enable engine when the given entity type is disabled', async () => { await dataClient.enable({ + ...defaultOptions, entityTypes: [EntityType.universal], - fieldHistoryLength: 1, }); expect(spyInit).not.toHaveBeenCalled(); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts index 6e5ee394aec54..073296dd2bd06 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/entity_store_data_client.ts @@ -23,6 +23,7 @@ import moment from 'moment'; import type { EntityDefinitionWithState } from '@kbn/entityManager-plugin/server/lib/entities/types'; import type { EntityDefinition } from '@kbn/entities-schema'; import type { estypes } from '@elastic/elasticsearch'; +import { merge } from '../../../../common/utils/objects/merge'; import { getEnabledStoreEntityTypes } from '../../../../common/entity_analytics/entity_store/utils'; import { EntityType } from '../../../../common/entity_analytics/types'; import type { ExperimentalFeatures } from '../../../../common'; @@ -47,7 +48,12 @@ import type { EngineComponentResource, } from '../../../../common/api/entity_analytics'; import { EngineDescriptorClient } from './saved_object/engine_descriptor'; -import { ENGINE_STATUS, ENTITY_STORE_STATUS, MAX_SEARCH_RESPONSE_SIZE } from './constants'; +import { + ENGINE_STATUS, + ENTITY_STORE_STATUS, + MAX_SEARCH_RESPONSE_SIZE, + defaultOptions, +} from './constants'; import { AssetCriticalityMigrationClient } from '../asset_criticality/asset_criticality_migration_client'; import { startEntityStoreFieldRetentionEnrichTask, @@ -94,7 +100,7 @@ import { createKeywordBuilderPipeline, deleteKeywordBuilderPipeline, } from '../../asset_inventory/ingest_pipelines'; -import { DEFAULT_INTERVAL } from './tasks/field_retention_enrichment/constants'; + import type { ApiKeyManager } from './auth/api_key'; // Workaround. TransformState type is wrong. The health type should be: TransformHealth from '@kbn/transform-plugin/common/types/transform_stats' @@ -135,18 +141,6 @@ interface SearchEntitiesParams { sortOrder: SortOrder; } -export const DEFAULT_INIT_ENTITY_STORE: InitEntityStoreRequestBody = { - indexPattern: '', - lookbackPeriod: '24h', - filter: '', - fieldHistoryLength: 10, - enrichPolicyExecutionInterval: DEFAULT_INTERVAL, -}; - -const DEFAULT_ENTITY_ENGINE: InitEntityEngineRequestBody & { lookbackPeriod?: string } = { - ...DEFAULT_INIT_ENTITY_STORE, -}; - export class EntityStoreDataClient { private engineClient: EngineDescriptorClient; private assetCriticalityMigrationClient: AssetCriticalityMigrationClient; @@ -234,25 +228,13 @@ export class EntityStoreDataClient { } public async enable( - requestBodyOverrides: Partial = {}, + requestBodyOverrides: InitEntityStoreRequestBody, { pipelineDebugMode = false }: { pipelineDebugMode?: boolean } = {} ): Promise { if (!this.options.taskManager) { throw new Error('Task Manager is not available'); } - const { - indexPattern, - lookbackPeriod, - filter, - fieldHistoryLength, - entityTypes, - enrichPolicyExecutionInterval, - } = { - ...DEFAULT_INIT_ENTITY_STORE, - ...requestBodyOverrides, - }; - // Immediately defer the initialization to the next tick. This way we don't block on the init preflight checks const run = (fn: () => Promise) => new Promise((resolve) => setTimeout(() => fn().then(resolve), 0)); @@ -261,24 +243,14 @@ export class EntityStoreDataClient { const enabledEntityTypes = getEnabledStoreEntityTypes(experimentalFeatures); // When entityTypes param is defined it only enables the engines that are provided - const enginesTypes = entityTypes - ? (entityTypes as EntityType[]).filter((type) => enabledEntityTypes.includes(type)) + const enginesTypes = requestBodyOverrides.entityTypes + ? (requestBodyOverrides.entityTypes as EntityType[]).filter((type) => + enabledEntityTypes.includes(type) + ) : enabledEntityTypes; const promises = enginesTypes.map((entity) => - run(() => - this.init( - entity, - { - indexPattern, - lookbackPeriod, - filter, - fieldHistoryLength, - enrichPolicyExecutionInterval, - }, - { pipelineDebugMode } - ) - ) + run(() => this.init(entity, requestBodyOverrides, { pipelineDebugMode })) ); const engines = await Promise.all(promises); @@ -335,21 +307,9 @@ export class EntityStoreDataClient { public async init( entityType: EntityType, - InitEntityEngineRequestBodyOverrides: Partial = {}, + requestBody: InitEntityEngineRequestBody, { pipelineDebugMode = false }: { pipelineDebugMode?: boolean } = {} ): Promise { - const mergedRequest = { - ...DEFAULT_ENTITY_ENGINE, - ...InitEntityEngineRequestBodyOverrides, - } as Required; - - const { - indexPattern, - filter, - fieldHistoryLength, - lookbackPeriod, - enrichPolicyExecutionInterval, - } = mergedRequest; const { experimentalFeatures } = this.options; if (entityType === EntityType.universal && !experimentalFeatures.assetInventoryStoreEnabled) { @@ -394,23 +354,14 @@ export class EntityStoreDataClient { 'Initializing entity engine' ); - const descriptor = await this.engineClient.init(entityType, { - filter, - fieldHistoryLength, - lookbackPeriod, - indexPattern, - }); + const descriptor = await this.engineClient.init(entityType, requestBody); this.log('debug', entityType, `Initialized engine saved object`); this.asyncSetup( entityType, - fieldHistoryLength, - lookbackPeriod, - enrichPolicyExecutionInterval, this.options.taskManager, - indexPattern, - filter, config, + requestBody, pipelineDebugMode ).catch((e) => this.log('error', entityType, `Error during async setup of entity store: ${e.message}`) @@ -421,24 +372,21 @@ export class EntityStoreDataClient { private async asyncSetup( entityType: EntityType, - fieldHistoryLength: number, - lookbackPeriod: string, - enrichPolicyExecutionInterval: string, taskManager: TaskManagerStartContract, - indexPattern: string, - filter: string, config: EntityStoreConfig, + requestParams: InitEntityEngineRequestBody, pipelineDebugMode: boolean ) { const setupStartTime = moment().utc().toISOString(); const { logger, namespace, appClient, dataViewsService } = this.options; try { const defaultIndexPatterns = await buildIndexPatterns(namespace, appClient, dataViewsService); + const options = merge(defaultOptions, requestParams); const description = createEngineDescription({ entityType, namespace, - requestParams: { indexPattern, fieldHistoryLength, lookbackPeriod }, + requestParams, defaultIndexPatterns, config, }); @@ -453,7 +401,7 @@ export class EntityStoreDataClient { // set up the entity manager definition const definition = convertToEntityManagerDefinition(description, { namespace, - filter, + filter: options.filter, }); await this.entityClient.createEntityDefinition({ @@ -516,7 +464,7 @@ export class EntityStoreDataClient { namespace, logger, taskManager, - interval: enrichPolicyExecutionInterval, + interval: options.enrichPolicyExecutionInterval, }); // this task will continuously refresh the Entity Store indices based on the Data View diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/installation/__snapshots__/engine_description.test.ts.snap b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/installation/__snapshots__/engine_description.test.ts.snap new file mode 100644 index 0000000000000..c792dffc19802 --- /dev/null +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/installation/__snapshots__/engine_description.test.ts.snap @@ -0,0 +1,662 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getUnitedEntityDefinition host entityManagerDefinition 1`] = ` +Object { + "displayNameTemplate": "{{host.name}}", + "id": "security_host_test", + "identityFields": Array [ + Object { + "field": "host.name", + "optional": false, + }, + ], + "indexPatterns": Array [ + "test*", + ], + "latest": Object { + "lookbackPeriod": "24h", + "settings": Object { + "docsPerSecond": undefined, + "frequency": "1m", + "syncDelay": "1m", + "syncField": "@timestamp", + "timeout": "180s", + }, + "timestampField": "@timestamp", + }, + "managed": true, + "metadata": Array [ + Object { + "aggregation": Object { + "limit": 10, + "type": "terms", + }, + "destination": "host.domain", + "source": "host.domain", + }, + Object { + "aggregation": Object { + "limit": 10, + "type": "terms", + }, + "destination": "host.hostname", + "source": "host.hostname", + }, + Object { + "aggregation": Object { + "limit": 10, + "type": "terms", + }, + "destination": "host.id", + "source": "host.id", + }, + Object { + "aggregation": Object { + "limit": 10, + "type": "terms", + }, + "destination": "host.os.name", + "source": "host.os.name", + }, + Object { + "aggregation": Object { + "limit": 10, + "type": "terms", + }, + "destination": "host.os.type", + "source": "host.os.type", + }, + Object { + "aggregation": Object { + "limit": 10, + "type": "terms", + }, + "destination": "host.ip", + "source": "host.ip", + }, + Object { + "aggregation": Object { + "limit": 10, + "type": "terms", + }, + "destination": "host.mac", + "source": "host.mac", + }, + Object { + "aggregation": Object { + "limit": 10, + "type": "terms", + }, + "destination": "host.type", + "source": "host.type", + }, + Object { + "aggregation": Object { + "limit": 10, + "type": "terms", + }, + "destination": "host.architecture", + "source": "host.architecture", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "asc", + }, + "type": "top_value", + }, + "destination": "entity.source", + "source": "_index", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "asset.criticality", + "source": "asset.criticality", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "host.risk.calculated_level", + "source": "host.risk.calculated_level", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "host.risk.calculated_score", + "source": "host.risk.calculated_score", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "host.risk.calculated_score_norm", + "source": "host.risk.calculated_score_norm", + }, + ], + "name": "Security 'host' Entity Store Definition", + "type": "host", + "version": "1.0.0", +} +`; + +exports[`getUnitedEntityDefinition host mapping 1`] = ` +Object { + "properties": Object { + "@timestamp": Object { + "type": "date", + }, + "asset.criticality": Object { + "type": "keyword", + }, + "entity.name": Object { + "fields": Object { + "text": Object { + "type": "match_only_text", + }, + }, + "type": "keyword", + }, + "entity.source": Object { + "type": "keyword", + }, + "host.architecture": Object { + "type": "keyword", + }, + "host.domain": Object { + "type": "keyword", + }, + "host.hostname": Object { + "type": "keyword", + }, + "host.id": Object { + "type": "keyword", + }, + "host.ip": Object { + "type": "ip", + }, + "host.mac": Object { + "type": "keyword", + }, + "host.name": Object { + "fields": Object { + "text": Object { + "type": "match_only_text", + }, + }, + "type": "keyword", + }, + "host.os.name": Object { + "fields": Object { + "text": Object { + "type": "match_only_text", + }, + }, + "type": "keyword", + }, + "host.os.type": Object { + "type": "keyword", + }, + "host.risk.calculated_level": Object { + "type": "keyword", + }, + "host.risk.calculated_score": Object { + "type": "float", + }, + "host.risk.calculated_score_norm": Object { + "type": "float", + }, + "host.type": Object { + "type": "keyword", + }, + }, +} +`; + +exports[`getUnitedEntityDefinition service entityManagerDefinition 1`] = ` +Object { + "displayNameTemplate": "{{service.name}}", + "id": "security_service_test", + "identityFields": Array [ + Object { + "field": "service.name", + "optional": false, + }, + ], + "indexPatterns": Array [ + "test*", + ], + "latest": Object { + "lookbackPeriod": "24h", + "settings": Object { + "docsPerSecond": undefined, + "frequency": "1m", + "syncDelay": "1m", + "syncField": "@timestamp", + "timeout": "180s", + }, + "timestampField": "@timestamp", + }, + "managed": true, + "metadata": Array [ + Object { + "aggregation": Object { + "limit": 10, + "type": "terms", + }, + "destination": "service.address", + "source": "service.address", + }, + Object { + "aggregation": Object { + "limit": 10, + "type": "terms", + }, + "destination": "service.environment", + "source": "service.environment", + }, + Object { + "aggregation": Object { + "limit": 10, + "type": "terms", + }, + "destination": "service.ephemeral_id", + "source": "service.ephemeral_id", + }, + Object { + "aggregation": Object { + "limit": 10, + "type": "terms", + }, + "destination": "service.id", + "source": "service.id", + }, + Object { + "aggregation": Object { + "limit": 10, + "type": "terms", + }, + "destination": "service.node.name", + "source": "service.node.name", + }, + Object { + "aggregation": Object { + "limit": 10, + "type": "terms", + }, + "destination": "service.node.roles", + "source": "service.node.roles", + }, + Object { + "aggregation": Object { + "limit": 10, + "type": "terms", + }, + "destination": "service.node.role", + "source": "service.node.role", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "service.state", + "source": "service.state", + }, + Object { + "aggregation": Object { + "limit": 10, + "type": "terms", + }, + "destination": "service.type", + "source": "service.type", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "service.version", + "source": "service.version", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "asc", + }, + "type": "top_value", + }, + "destination": "entity.source", + "source": "_index", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "asset.criticality", + "source": "asset.criticality", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "service.risk.calculated_level", + "source": "service.risk.calculated_level", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "service.risk.calculated_score", + "source": "service.risk.calculated_score", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "service.risk.calculated_score_norm", + "source": "service.risk.calculated_score_norm", + }, + ], + "name": "Security 'service' Entity Store Definition", + "type": "service", + "version": "1.0.0", +} +`; + +exports[`getUnitedEntityDefinition service mapping 1`] = ` +Object { + "properties": Object { + "@timestamp": Object { + "type": "date", + }, + "asset.criticality": Object { + "type": "keyword", + }, + "entity.name": Object { + "fields": Object { + "text": Object { + "type": "match_only_text", + }, + }, + "type": "keyword", + }, + "entity.source": Object { + "type": "keyword", + }, + "service.address": Object { + "type": "keyword", + }, + "service.environment": Object { + "type": "keyword", + }, + "service.ephemeral_id": Object { + "type": "keyword", + }, + "service.id": Object { + "type": "keyword", + }, + "service.name": Object { + "fields": Object { + "text": Object { + "type": "match_only_text", + }, + }, + "type": "keyword", + }, + "service.node.name": Object { + "type": "keyword", + }, + "service.node.role": Object { + "type": "keyword", + }, + "service.node.roles": Object { + "type": "keyword", + }, + "service.risk.calculated_level": Object { + "type": "keyword", + }, + "service.risk.calculated_score": Object { + "type": "float", + }, + "service.risk.calculated_score_norm": Object { + "type": "float", + }, + "service.state": Object { + "type": "keyword", + }, + "service.type": Object { + "type": "keyword", + }, + "service.version": Object { + "type": "keyword", + }, + }, +} +`; + +exports[`getUnitedEntityDefinition user entityManagerDefinition 1`] = ` +Object { + "displayNameTemplate": "{{user.name}}", + "id": "security_user_test", + "identityFields": Array [ + Object { + "field": "user.name", + "optional": false, + }, + ], + "indexPatterns": Array [ + "test*", + ], + "latest": Object { + "lookbackPeriod": "24h", + "settings": Object { + "docsPerSecond": undefined, + "frequency": "1m", + "syncDelay": "1m", + "syncField": "@timestamp", + "timeout": "180s", + }, + "timestampField": "@timestamp", + }, + "managed": true, + "metadata": Array [ + Object { + "aggregation": Object { + "limit": 10, + "type": "terms", + }, + "destination": "user.domain", + "source": "user.domain", + }, + Object { + "aggregation": Object { + "limit": 10, + "type": "terms", + }, + "destination": "user.email", + "source": "user.email", + }, + Object { + "aggregation": Object { + "limit": 10, + "type": "terms", + }, + "destination": "user.full_name", + "source": "user.full_name", + }, + Object { + "aggregation": Object { + "limit": 10, + "type": "terms", + }, + "destination": "user.hash", + "source": "user.hash", + }, + Object { + "aggregation": Object { + "limit": 10, + "type": "terms", + }, + "destination": "user.id", + "source": "user.id", + }, + Object { + "aggregation": Object { + "limit": 10, + "type": "terms", + }, + "destination": "user.roles", + "source": "user.roles", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "asc", + }, + "type": "top_value", + }, + "destination": "entity.source", + "source": "_index", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "asset.criticality", + "source": "asset.criticality", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "user.risk.calculated_level", + "source": "user.risk.calculated_level", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "user.risk.calculated_score", + "source": "user.risk.calculated_score", + }, + Object { + "aggregation": Object { + "sort": Object { + "@timestamp": "desc", + }, + "type": "top_value", + }, + "destination": "user.risk.calculated_score_norm", + "source": "user.risk.calculated_score_norm", + }, + ], + "name": "Security 'user' Entity Store Definition", + "type": "user", + "version": "1.0.0", +} +`; + +exports[`getUnitedEntityDefinition user mapping 1`] = ` +Object { + "properties": Object { + "@timestamp": Object { + "type": "date", + }, + "asset.criticality": Object { + "type": "keyword", + }, + "entity.name": Object { + "fields": Object { + "text": Object { + "type": "match_only_text", + }, + }, + "type": "keyword", + }, + "entity.source": Object { + "type": "keyword", + }, + "user.domain": Object { + "type": "keyword", + }, + "user.email": Object { + "type": "keyword", + }, + "user.full_name": Object { + "fields": Object { + "text": Object { + "type": "match_only_text", + }, + }, + "type": "keyword", + }, + "user.hash": Object { + "type": "keyword", + }, + "user.id": Object { + "type": "keyword", + }, + "user.name": Object { + "fields": Object { + "text": Object { + "type": "match_only_text", + }, + }, + "type": "keyword", + }, + "user.risk.calculated_level": Object { + "type": "keyword", + }, + "user.risk.calculated_score": Object { + "type": "float", + }, + "user.risk.calculated_score_norm": Object { + "type": "float", + }, + "user.roles": Object { + "type": "keyword", + }, + }, +} +`; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/installation/engine_description.test.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/installation/engine_description.test.ts index 480dadf676f3c..3e05d1f992ab2 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/installation/engine_description.test.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/installation/engine_description.test.ts @@ -8,6 +8,7 @@ import { duration } from 'moment'; import { createEngineDescription } from './engine_description'; import { convertToEntityManagerDefinition } from '../entity_definitions/entity_manager_conversion'; +import { defaultOptions } from '../constants'; describe('getUnitedEntityDefinition', () => { const defaultIndexPatterns = ['test*']; @@ -15,9 +16,7 @@ describe('getUnitedEntityDefinition', () => { const description = createEngineDescription({ entityType: 'host', namespace: 'test', - requestParams: { - fieldHistoryLength: 10, - }, + requestParams: defaultOptions, defaultIndexPatterns, config: { syncDelay: duration(60, 'seconds'), @@ -27,78 +26,7 @@ describe('getUnitedEntityDefinition', () => { }); it('mapping', () => { - expect(description.indexMappings).toMatchInlineSnapshot(` - Object { - "properties": Object { - "@timestamp": Object { - "type": "date", - }, - "asset.criticality": Object { - "type": "keyword", - }, - "entity.name": Object { - "fields": Object { - "text": Object { - "type": "match_only_text", - }, - }, - "type": "keyword", - }, - "entity.source": Object { - "type": "keyword", - }, - "host.architecture": Object { - "type": "keyword", - }, - "host.domain": Object { - "type": "keyword", - }, - "host.hostname": Object { - "type": "keyword", - }, - "host.id": Object { - "type": "keyword", - }, - "host.ip": Object { - "type": "ip", - }, - "host.mac": Object { - "type": "keyword", - }, - "host.name": Object { - "fields": Object { - "text": Object { - "type": "match_only_text", - }, - }, - "type": "keyword", - }, - "host.os.name": Object { - "fields": Object { - "text": Object { - "type": "match_only_text", - }, - }, - "type": "keyword", - }, - "host.os.type": Object { - "type": "keyword", - }, - "host.risk.calculated_level": Object { - "type": "keyword", - }, - "host.risk.calculated_score": Object { - "type": "float", - }, - "host.risk.calculated_score_norm": Object { - "type": "float", - }, - "host.type": Object { - "type": "keyword", - }, - }, - } - `); + expect(description.indexMappings).toMatchSnapshot(); }); it('entityManagerDefinition', () => { @@ -107,167 +35,14 @@ describe('getUnitedEntityDefinition', () => { filter: '', }); - expect(entityManagerDefinition).toMatchInlineSnapshot(` - Object { - "displayNameTemplate": "{{host.name}}", - "id": "security_host_test", - "identityFields": Array [ - Object { - "field": "host.name", - "optional": false, - }, - ], - "indexPatterns": Array [ - "test*", - ], - "latest": Object { - "lookbackPeriod": "1d", - "settings": Object { - "frequency": "60s", - "syncDelay": "60s", - "syncField": "@timestamp", - }, - "timestampField": "@timestamp", - }, - "managed": true, - "metadata": Array [ - Object { - "aggregation": Object { - "limit": 10, - "type": "terms", - }, - "destination": "host.domain", - "source": "host.domain", - }, - Object { - "aggregation": Object { - "limit": 10, - "type": "terms", - }, - "destination": "host.hostname", - "source": "host.hostname", - }, - Object { - "aggregation": Object { - "limit": 10, - "type": "terms", - }, - "destination": "host.id", - "source": "host.id", - }, - Object { - "aggregation": Object { - "limit": 10, - "type": "terms", - }, - "destination": "host.os.name", - "source": "host.os.name", - }, - Object { - "aggregation": Object { - "limit": 10, - "type": "terms", - }, - "destination": "host.os.type", - "source": "host.os.type", - }, - Object { - "aggregation": Object { - "limit": 10, - "type": "terms", - }, - "destination": "host.ip", - "source": "host.ip", - }, - Object { - "aggregation": Object { - "limit": 10, - "type": "terms", - }, - "destination": "host.mac", - "source": "host.mac", - }, - Object { - "aggregation": Object { - "limit": 10, - "type": "terms", - }, - "destination": "host.type", - "source": "host.type", - }, - Object { - "aggregation": Object { - "limit": 10, - "type": "terms", - }, - "destination": "host.architecture", - "source": "host.architecture", - }, - Object { - "aggregation": Object { - "sort": Object { - "@timestamp": "asc", - }, - "type": "top_value", - }, - "destination": "entity.source", - "source": "_index", - }, - Object { - "aggregation": Object { - "sort": Object { - "@timestamp": "desc", - }, - "type": "top_value", - }, - "destination": "asset.criticality", - "source": "asset.criticality", - }, - Object { - "aggregation": Object { - "sort": Object { - "@timestamp": "desc", - }, - "type": "top_value", - }, - "destination": "host.risk.calculated_level", - "source": "host.risk.calculated_level", - }, - Object { - "aggregation": Object { - "sort": Object { - "@timestamp": "desc", - }, - "type": "top_value", - }, - "destination": "host.risk.calculated_score", - "source": "host.risk.calculated_score", - }, - Object { - "aggregation": Object { - "sort": Object { - "@timestamp": "desc", - }, - "type": "top_value", - }, - "destination": "host.risk.calculated_score_norm", - "source": "host.risk.calculated_score_norm", - }, - ], - "name": "Security 'host' Entity Store Definition", - "type": "host", - "version": "1.0.0", - } - `); + expect(entityManagerDefinition).toMatchSnapshot(); }); }); describe('user', () => { const description = createEngineDescription({ entityType: 'user', namespace: 'test', - requestParams: { - fieldHistoryLength: 10, - }, + requestParams: defaultOptions, defaultIndexPatterns, config: { syncDelay: duration(60, 'seconds'), @@ -277,203 +52,14 @@ describe('getUnitedEntityDefinition', () => { }); it('mapping', () => { - expect(description.indexMappings).toMatchInlineSnapshot(` - Object { - "properties": Object { - "@timestamp": Object { - "type": "date", - }, - "asset.criticality": Object { - "type": "keyword", - }, - "entity.name": Object { - "fields": Object { - "text": Object { - "type": "match_only_text", - }, - }, - "type": "keyword", - }, - "entity.source": Object { - "type": "keyword", - }, - "user.domain": Object { - "type": "keyword", - }, - "user.email": Object { - "type": "keyword", - }, - "user.full_name": Object { - "fields": Object { - "text": Object { - "type": "match_only_text", - }, - }, - "type": "keyword", - }, - "user.hash": Object { - "type": "keyword", - }, - "user.id": Object { - "type": "keyword", - }, - "user.name": Object { - "fields": Object { - "text": Object { - "type": "match_only_text", - }, - }, - "type": "keyword", - }, - "user.risk.calculated_level": Object { - "type": "keyword", - }, - "user.risk.calculated_score": Object { - "type": "float", - }, - "user.risk.calculated_score_norm": Object { - "type": "float", - }, - "user.roles": Object { - "type": "keyword", - }, - }, - } - `); + expect(description.indexMappings).toMatchSnapshot(); }); it('entityManagerDefinition', () => { const entityManagerDefinition = convertToEntityManagerDefinition(description, { namespace: 'test', filter: '', }); - expect(entityManagerDefinition).toMatchInlineSnapshot(` - Object { - "displayNameTemplate": "{{user.name}}", - "id": "security_user_test", - "identityFields": Array [ - Object { - "field": "user.name", - "optional": false, - }, - ], - "indexPatterns": Array [ - "test*", - ], - "latest": Object { - "lookbackPeriod": "1d", - "settings": Object { - "frequency": "60s", - "syncDelay": "60s", - "syncField": "@timestamp", - }, - "timestampField": "@timestamp", - }, - "managed": true, - "metadata": Array [ - Object { - "aggregation": Object { - "limit": 10, - "type": "terms", - }, - "destination": "user.domain", - "source": "user.domain", - }, - Object { - "aggregation": Object { - "limit": 10, - "type": "terms", - }, - "destination": "user.email", - "source": "user.email", - }, - Object { - "aggregation": Object { - "limit": 10, - "type": "terms", - }, - "destination": "user.full_name", - "source": "user.full_name", - }, - Object { - "aggregation": Object { - "limit": 10, - "type": "terms", - }, - "destination": "user.hash", - "source": "user.hash", - }, - Object { - "aggregation": Object { - "limit": 10, - "type": "terms", - }, - "destination": "user.id", - "source": "user.id", - }, - Object { - "aggregation": Object { - "limit": 10, - "type": "terms", - }, - "destination": "user.roles", - "source": "user.roles", - }, - Object { - "aggregation": Object { - "sort": Object { - "@timestamp": "asc", - }, - "type": "top_value", - }, - "destination": "entity.source", - "source": "_index", - }, - Object { - "aggregation": Object { - "sort": Object { - "@timestamp": "desc", - }, - "type": "top_value", - }, - "destination": "asset.criticality", - "source": "asset.criticality", - }, - Object { - "aggregation": Object { - "sort": Object { - "@timestamp": "desc", - }, - "type": "top_value", - }, - "destination": "user.risk.calculated_level", - "source": "user.risk.calculated_level", - }, - Object { - "aggregation": Object { - "sort": Object { - "@timestamp": "desc", - }, - "type": "top_value", - }, - "destination": "user.risk.calculated_score", - "source": "user.risk.calculated_score", - }, - Object { - "aggregation": Object { - "sort": Object { - "@timestamp": "desc", - }, - "type": "top_value", - }, - "destination": "user.risk.calculated_score_norm", - "source": "user.risk.calculated_score_norm", - }, - ], - "name": "Security 'user' Entity Store Definition", - "type": "user", - "version": "1.0.0", - } - `); + expect(entityManagerDefinition).toMatchSnapshot(); }); }); @@ -481,9 +67,7 @@ describe('getUnitedEntityDefinition', () => { const description = createEngineDescription({ entityType: 'service', namespace: 'test', - requestParams: { - fieldHistoryLength: 10, - }, + requestParams: defaultOptions, defaultIndexPatterns, config: { syncDelay: duration(60, 'seconds'), @@ -493,76 +77,7 @@ describe('getUnitedEntityDefinition', () => { }); it('mapping', () => { - expect(description.indexMappings).toMatchInlineSnapshot(` - Object { - "properties": Object { - "@timestamp": Object { - "type": "date", - }, - "asset.criticality": Object { - "type": "keyword", - }, - "entity.name": Object { - "fields": Object { - "text": Object { - "type": "match_only_text", - }, - }, - "type": "keyword", - }, - "entity.source": Object { - "type": "keyword", - }, - "service.address": Object { - "type": "keyword", - }, - "service.environment": Object { - "type": "keyword", - }, - "service.ephemeral_id": Object { - "type": "keyword", - }, - "service.id": Object { - "type": "keyword", - }, - "service.name": Object { - "fields": Object { - "text": Object { - "type": "match_only_text", - }, - }, - "type": "keyword", - }, - "service.node.name": Object { - "type": "keyword", - }, - "service.node.role": Object { - "type": "keyword", - }, - "service.node.roles": Object { - "type": "keyword", - }, - "service.risk.calculated_level": Object { - "type": "keyword", - }, - "service.risk.calculated_score": Object { - "type": "float", - }, - "service.risk.calculated_score_norm": Object { - "type": "float", - }, - "service.state": Object { - "type": "keyword", - }, - "service.type": Object { - "type": "keyword", - }, - "service.version": Object { - "type": "keyword", - }, - }, - } - `); + expect(description.indexMappings).toMatchSnapshot(); }); it('entityManagerDefinition', () => { @@ -570,170 +85,7 @@ describe('getUnitedEntityDefinition', () => { namespace: 'test', filter: '', }); - expect(entityManagerDefinition).toMatchInlineSnapshot(` - Object { - "displayNameTemplate": "{{service.name}}", - "id": "security_service_test", - "identityFields": Array [ - Object { - "field": "service.name", - "optional": false, - }, - ], - "indexPatterns": Array [ - "test*", - ], - "latest": Object { - "lookbackPeriod": "1d", - "settings": Object { - "frequency": "60s", - "syncDelay": "60s", - "syncField": "@timestamp", - }, - "timestampField": "@timestamp", - }, - "managed": true, - "metadata": Array [ - Object { - "aggregation": Object { - "limit": 10, - "type": "terms", - }, - "destination": "service.address", - "source": "service.address", - }, - Object { - "aggregation": Object { - "limit": 10, - "type": "terms", - }, - "destination": "service.environment", - "source": "service.environment", - }, - Object { - "aggregation": Object { - "limit": 10, - "type": "terms", - }, - "destination": "service.ephemeral_id", - "source": "service.ephemeral_id", - }, - Object { - "aggregation": Object { - "limit": 10, - "type": "terms", - }, - "destination": "service.id", - "source": "service.id", - }, - Object { - "aggregation": Object { - "limit": 10, - "type": "terms", - }, - "destination": "service.node.name", - "source": "service.node.name", - }, - Object { - "aggregation": Object { - "limit": 10, - "type": "terms", - }, - "destination": "service.node.roles", - "source": "service.node.roles", - }, - Object { - "aggregation": Object { - "limit": 10, - "type": "terms", - }, - "destination": "service.node.role", - "source": "service.node.role", - }, - Object { - "aggregation": Object { - "sort": Object { - "@timestamp": "desc", - }, - "type": "top_value", - }, - "destination": "service.state", - "source": "service.state", - }, - Object { - "aggregation": Object { - "limit": 10, - "type": "terms", - }, - "destination": "service.type", - "source": "service.type", - }, - Object { - "aggregation": Object { - "sort": Object { - "@timestamp": "desc", - }, - "type": "top_value", - }, - "destination": "service.version", - "source": "service.version", - }, - Object { - "aggregation": Object { - "sort": Object { - "@timestamp": "asc", - }, - "type": "top_value", - }, - "destination": "entity.source", - "source": "_index", - }, - Object { - "aggregation": Object { - "sort": Object { - "@timestamp": "desc", - }, - "type": "top_value", - }, - "destination": "asset.criticality", - "source": "asset.criticality", - }, - Object { - "aggregation": Object { - "sort": Object { - "@timestamp": "desc", - }, - "type": "top_value", - }, - "destination": "service.risk.calculated_level", - "source": "service.risk.calculated_level", - }, - Object { - "aggregation": Object { - "sort": Object { - "@timestamp": "desc", - }, - "type": "top_value", - }, - "destination": "service.risk.calculated_score", - "source": "service.risk.calculated_score", - }, - Object { - "aggregation": Object { - "sort": Object { - "@timestamp": "desc", - }, - "type": "top_value", - }, - "destination": "service.risk.calculated_score_norm", - "source": "service.risk.calculated_score_norm", - }, - ], - "name": "Security 'service' Entity Store Definition", - "type": "service", - "version": "1.0.0", - } - `); + expect(entityManagerDefinition).toMatchSnapshot(); }); }); }); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/installation/engine_description.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/installation/engine_description.ts index 83c738f2c8895..c5c591e6ab3b1 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/installation/engine_description.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/installation/engine_description.ts @@ -4,17 +4,14 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { pipe } from 'fp-ts/lib/function'; -import { assign, concat, map, merge, update } from 'lodash/fp'; -import { set } from '@kbn/safer-lodash-set/fp'; +import { assign, concat } from 'lodash/fp'; -import type { EntityType } from '../../../../../common/api/entity_analytics'; -import { - DEFAULT_FIELD_HISTORY_LENGTH, - DEFAULT_LOOKBACK_PERIOD, - DEFAULT_TIMESTAMP_FIELD, -} from '../entity_definitions/constants'; +import type { + EntityType, + InitEntityEngineRequestBody, +} from '../../../../../common/api/entity_analytics'; +import { DEFAULT_TIMESTAMP_FIELD } from '../entity_definitions/constants'; import { generateIndexMappings } from '../elasticsearch_assets'; import { hostEntityEngineDescription, @@ -28,6 +25,9 @@ import { buildEntityDefinitionId } from '../utils'; import type { EntityDescription } from '../entity_definitions/types'; import type { EntityEngineInstallationDescriptor } from './types'; +import { merge } from '../../../../../common/utils/objects/merge'; +import { defaultOptions } from '../constants'; + const engineDescriptionRegistry: Record = { host: hostEntityEngineDescription, user: userEntityEngineDescription, @@ -39,62 +39,52 @@ interface EngineDescriptionParams { entityType: EntityType; namespace: string; config: EntityStoreConfig; - requestParams?: { - indexPattern?: string; - fieldHistoryLength?: number; - lookbackPeriod?: string; - }; + requestParams?: InitEntityEngineRequestBody; defaultIndexPatterns: string[]; } -export const createEngineDescription = (options: EngineDescriptionParams) => { - const { entityType, namespace, config, requestParams, defaultIndexPatterns } = options; - const fieldHistoryLength = requestParams?.fieldHistoryLength || DEFAULT_FIELD_HISTORY_LENGTH; +export const createEngineDescription = (params: EngineDescriptionParams) => { + const { entityType, namespace, config, requestParams = {}, defaultIndexPatterns } = params; + + const fileConfig = { + delay: `${config.syncDelay.asSeconds()}s`, + frequency: `${config.frequency.asSeconds()}s`, + }; + const options = merge(defaultOptions, merge(fileConfig, requestParams)); - const indexPatterns = requestParams?.indexPattern - ? defaultIndexPatterns.concat(requestParams?.indexPattern.split(',')) + const indexPatterns = options.indexPattern + ? defaultIndexPatterns.concat(options.indexPattern.split(',')) : defaultIndexPatterns; const description = engineDescriptionRegistry[entityType]; const settings: EntityEngineInstallationDescriptor['settings'] = { - syncDelay: `${config.syncDelay.asSeconds()}s`, - frequency: `${config.frequency.asSeconds()}s`, - lookbackPeriod: - requestParams?.lookbackPeriod || - description.settings?.lookbackPeriod || - DEFAULT_LOOKBACK_PERIOD, + syncDelay: options.delay, + timeout: options.timeout, + frequency: options.frequency, + docsPerSecond: options.docsPerSecond, + lookbackPeriod: options.lookbackPeriod, timestampField: description.settings?.timestampField || DEFAULT_TIMESTAMP_FIELD, }; - const updatedDescription = pipe( - description, - set('id', buildEntityDefinitionId(entityType, namespace)), - update('settings', assign(settings)), - updateIndexPatterns(indexPatterns), - updateRetentionFields(fieldHistoryLength), - setDefaultDynamic, - addIndexMappings - ) as EntityEngineInstallationDescriptor; - - return updatedDescription; -}; - -const updateIndexPatterns = (indexPatterns: string[]) => - update('indexPatterns', (prev = []) => concat(indexPatterns, prev)); - -const updateRetentionFields = (fieldHistoryLength: number) => - update( - 'fields', - map( + const defaults = { + ...description, + id: buildEntityDefinitionId(entityType, namespace), + settings: assign(settings, description.settings), + indexPatterns: concat(indexPatterns, (description.indexPatterns || []) as string[]), + fields: description.fields.map( merge({ - retention: { maxLength: fieldHistoryLength }, - aggregation: { limit: fieldHistoryLength }, + retention: { maxLength: options.fieldHistoryLength }, + aggregation: { limit: options.fieldHistoryLength }, }) - ) - ); + ), + dynamic: description.dynamic || false, + }; -const addIndexMappings = (description: EntityEngineInstallationDescriptor) => - set('indexMappings', generateIndexMappings(description), description); + const updatedDescription: EntityEngineInstallationDescriptor = { + ...defaults, + indexMappings: generateIndexMappings(defaults), + }; -const setDefaultDynamic = update('dynamic', (dynamic = false) => dynamic); + return updatedDescription; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/installation/types.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/installation/types.ts index 7ec798ca26fbf..ca7859c867c99 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/installation/types.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/installation/types.ts @@ -47,6 +47,8 @@ export interface EntityEngineInstallationDescriptor { settings: { syncDelay: string; frequency: string; + timeout: string; + docsPerSecond?: number; lookbackPeriod: string; timestampField: string; }; @@ -56,7 +58,7 @@ export interface EntityEngineInstallationDescriptor { * This can be an array of processors which get appended to the default pipeline, * or a function that takes the default processors and returns an array of processors. **/ - pipeline: + pipeline?: | IngestProcessorContainer[] | ((defaultProcessors: IngestProcessorContainer[]) => IngestProcessorContainer[]); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/saved_object/engine_descriptor.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/saved_object/engine_descriptor.ts index ccf25e7cede99..97f5c10268841 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/saved_object/engine_descriptor.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/saved_object/engine_descriptor.ts @@ -9,6 +9,8 @@ import type { SavedObjectsClientContract, SavedObjectsFindResponse, } from '@kbn/core-saved-objects-api-server'; +import { merge } from 'lodash/fp'; +import type { InitEntityEngineRequestBody } from '../../../../../common/api/entity_analytics/entity_store/engine/init.gen'; import type { EngineDescriptor, EngineStatus, @@ -17,13 +19,15 @@ import type { import { entityEngineDescriptorTypeName } from './engine_descriptor_type'; import { getByEntityTypeQuery } from '../utils'; -import { ENGINE_STATUS } from '../constants'; +import { ENGINE_STATUS, defaultOptions } from '../constants'; interface EngineDescriptorDependencies { soClient: SavedObjectsClientContract; namespace: string; } +type InitOptions = InitEntityEngineRequestBody; + export class EngineDescriptorClient { constructor(private readonly deps: EngineDescriptorDependencies) {} @@ -31,15 +35,8 @@ export class EngineDescriptorClient { return `entity-engine-descriptor-${entityType}-${this.deps.namespace}`; } - async init( - entityType: EntityType, - { - filter, - fieldHistoryLength, - indexPattern, - lookbackPeriod, - }: { filter: string; fieldHistoryLength: number; indexPattern: string; lookbackPeriod: string } - ) { + async init(entityType: EntityType, options: InitOptions) { + const opts: typeof defaultOptions = merge(defaultOptions, options); const engineDescriptor = await this.find(entityType); if (engineDescriptor.total > 1) { @@ -52,10 +49,7 @@ export class EngineDescriptorClient { ...old, error: undefined, // if the engine is being re-initialized, clear any previous error status: ENGINE_STATUS.INSTALLING, - filter, - fieldHistoryLength, - indexPattern, - lookbackPeriod, + ...opts, }; await this.deps.soClient.update( entityEngineDescriptorTypeName, @@ -72,10 +66,7 @@ export class EngineDescriptorClient { { status: ENGINE_STATUS.INSTALLING, type: entityType, - indexPattern, - filter, - fieldHistoryLength, - lookbackPeriod, + ...opts, }, { id: this.getSavedObjectId(entityType) } ); diff --git a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/saved_object/engine_descriptor_type.ts b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/saved_object/engine_descriptor_type.ts index 9f618521ce27a..008b9971975c5 100644 --- a/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/saved_object/engine_descriptor_type.ts +++ b/x-pack/solutions/security/plugins/security_solution/server/lib/entity_analytics/entity_store/saved_object/engine_descriptor_type.ts @@ -8,6 +8,7 @@ import type { SavedObjectsModelVersion } from '@kbn/core-saved-objects-server'; import { SECURITY_SOLUTION_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; import type { SavedObjectsType } from '@kbn/core/server'; +import { defaultOptions } from '../constants'; export const entityEngineDescriptorTypeName = 'entity-engine-status'; @@ -55,11 +56,27 @@ const version1: SavedObjectsModelVersion = { ], }; +const version2: SavedObjectsModelVersion = { + changes: [ + { + type: 'data_backfill', + backfillFn: (document) => { + return { + attributes: { + ...defaultOptions, + ...document.attributes, + }, + }; + }, + }, + ], +}; + export const entityEngineDescriptorType: SavedObjectsType = { name: entityEngineDescriptorTypeName, indexPattern: SECURITY_SOLUTION_SAVED_OBJECT_INDEX, hidden: false, namespaceType: 'multiple-isolated', mappings: entityEngineDescriptorTypeMappings, - modelVersions: { 1: version1 }, + modelVersions: { 1: version1, 2: version2 }, }; diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/entity_store.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/entity_store.ts index 1a47a01be7be9..5706dddef75e1 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/entity_store.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/entity_store.ts @@ -6,6 +6,8 @@ */ import expect from 'expect'; +import { defaultOptions } from '@kbn/security-solution-plugin/server/lib/entity_analytics/entity_store/constants'; +import { omit } from 'lodash/fp'; import { FtrProviderContext } from '../../../../ftr_provider_context'; import { EntityStoreUtils } from '../../utils'; import { dataViewRouteHelpersFactory } from '../../utils/data_view'; @@ -17,6 +19,8 @@ export default ({ getService }: FtrProviderContext) => { describe('@ess @skipInServerlessMKI Entity Store APIs', () => { const dataView = dataViewRouteHelpersFactory(supertest); + const defaults = omit('docsPerSecond', defaultOptions); + before(async () => { await utils.cleanEngines(); await dataView.create('security-solution'); @@ -85,12 +89,9 @@ export default ({ getService }: FtrProviderContext) => { .expect(200); expect(getResponse.body).toEqual({ + ...defaults, status: 'started', type: 'host', - indexPattern: '', - filter: '', - fieldHistoryLength: 10, - lookbackPeriod: '24h', }); }); @@ -102,12 +103,9 @@ export default ({ getService }: FtrProviderContext) => { .expect(200); expect(getResponse.body).toEqual({ + ...defaults, status: 'started', type: 'user', - indexPattern: '', - filter: '', - fieldHistoryLength: 10, - lookbackPeriod: '24h', }); }); }); @@ -121,20 +119,14 @@ export default ({ getService }: FtrProviderContext) => { expect(sortedEngines).toEqual([ { + ...defaults, status: 'started', type: 'host', - indexPattern: '', - filter: '', - fieldHistoryLength: 10, - lookbackPeriod: '24h', }, { + ...defaults, status: 'started', type: 'user', - indexPattern: '', - filter: '', - fieldHistoryLength: 10, - lookbackPeriod: '24h', }, ]); }); diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/entity_store_nondefault_spaces.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/entity_store_nondefault_spaces.ts index 2585fafc953a6..d9e2001286359 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/entity_store_nondefault_spaces.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/entity_store/trial_license_complete_tier/entity_store_nondefault_spaces.ts @@ -7,6 +7,8 @@ import expect from '@kbn/expect'; import { v4 as uuidv4 } from 'uuid'; +import { defaultOptions } from '@kbn/security-solution-plugin/server/lib/entity_analytics/entity_store/constants'; +import { omit } from 'lodash/fp'; import { FtrProviderContextWithSpaces } from '../../../../ftr_provider_context_with_spaces'; import { EntityStoreUtils } from '../../utils'; import { dataViewRouteHelpersFactory } from '../../utils/data_view'; @@ -21,6 +23,7 @@ export default ({ getService }: FtrProviderContextWithSpaces) => { describe('@ess Entity Store Engine APIs in non-default space', () => { const dataView = dataViewRouteHelpersFactory(supertest, namespace); + const defaults = omit('docsPerSecond', defaultOptions); before(async () => { await utils.cleanEngines(); await spaces.create({ @@ -73,12 +76,9 @@ export default ({ getService }: FtrProviderContextWithSpaces) => { .expect(200); expect(getResponse.body).to.eql({ + ...defaults, status: 'started', type: 'host', - filter: '', - fieldHistoryLength: 10, - lookbackPeriod: '24h', - indexPattern: '', }); }); @@ -93,12 +93,9 @@ export default ({ getService }: FtrProviderContextWithSpaces) => { .expect(200); expect(getResponse.body).to.eql({ + ...defaults, status: 'started', type: 'user', - filter: '', - fieldHistoryLength: 10, - lookbackPeriod: '24h', - indexPattern: '', }); }); }); @@ -112,20 +109,14 @@ export default ({ getService }: FtrProviderContextWithSpaces) => { expect(sortedEngines).to.eql([ { + ...defaults, status: 'started', type: 'host', - filter: '', - fieldHistoryLength: 10, - lookbackPeriod: '24h', - indexPattern: '', }, { + ...defaults, status: 'started', type: 'user', - filter: '', - fieldHistoryLength: 10, - lookbackPeriod: '24h', - indexPattern: '', }, ]); }); From 3430ab8246c600b4ba7db846a92665bd56c82882 Mon Sep 17 00:00:00 2001 From: Bharat Pasupula <123897612+bhapas@users.noreply.github.com> Date: Wed, 29 Jan 2025 21:40:56 +0100 Subject: [PATCH 11/12] [Automatic Import] Remove Tech preview badge for GA (#208523) --- .../translations/translations/fr-FR.json | 16 +++++++-------- .../translations/translations/ja-JP.json | 16 +++++++-------- .../translations/translations/zh-CN.json | 16 +++++++-------- .../automatic_import_card.tsx | 20 +------------------ .../translations.ts | 15 -------------- .../e2e/integrations_automatic_import.cy.ts | 2 -- .../screens/integrations_automatic_import.ts | 1 - .../plugins/shared/fleet/public/plugin.ts | 3 ++- 8 files changed, 27 insertions(+), 62 deletions(-) diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index 501e239b0095c..627b04a1dc5cc 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -9765,17 +9765,17 @@ "xpack.aiAssistant.chatTimeline.messages.system.label": "Système", "xpack.aiAssistant.chatTimeline.messages.user.label": "Vous", "xpack.aiAssistant.checkingKbAvailability": "Vérification de la disponibilité de la base de connaissances", - "xpack.aiAssistant.conversationList.deleteConversationIconLabel": "Supprimer", - "xpack.aiAssistant.conversationList.errorMessage": "Échec de chargement", - "xpack.aiAssistant.conversationList.noConversations": "Aucune conversation", - "xpack.aiAssistant.conversationList.dateGroupTitle.today": "Aujourd'hui", - "xpack.aiAssistant.conversationList.dateGroupTitle.yesterday": "Hier", - "xpack.aiAssistant.conversationList.dateGroupTitle.thisWeek": "Cette semaine", + "xpack.aiAssistant.conversationList.dateGroupTitle.lastMonth": "Le mois dernier", "xpack.aiAssistant.conversationList.dateGroupTitle.lastWeek": "La semaine dernière", + "xpack.aiAssistant.conversationList.dateGroupTitle.older": "Plus ancien", "xpack.aiAssistant.conversationList.dateGroupTitle.thisMonth": "Ce mois-ci", - "xpack.aiAssistant.conversationList.dateGroupTitle.lastMonth": "Le mois dernier", + "xpack.aiAssistant.conversationList.dateGroupTitle.thisWeek": "Cette semaine", "xpack.aiAssistant.conversationList.dateGroupTitle.thisYear": "Cette année", - "xpack.aiAssistant.conversationList.dateGroupTitle.older": "Plus ancien", + "xpack.aiAssistant.conversationList.dateGroupTitle.today": "Aujourd'hui", + "xpack.aiAssistant.conversationList.dateGroupTitle.yesterday": "Hier", + "xpack.aiAssistant.conversationList.deleteConversationIconLabel": "Supprimer", + "xpack.aiAssistant.conversationList.errorMessage": "Échec de chargement", + "xpack.aiAssistant.conversationList.noConversations": "Aucune conversation", "xpack.aiAssistant.conversationStartTitle": "a démarré une conversation", "xpack.aiAssistant.couldNotFindConversationContent": "Impossible de trouver une conversation avec l'ID {conversationId}. Assurez-vous que la conversation existe et que vous y avez accès.", "xpack.aiAssistant.couldNotFindConversationTitle": "Conversation introuvable", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index 18025161122ab..4d534f2d87e74 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -9641,17 +9641,17 @@ "xpack.aiAssistant.chatTimeline.messages.system.label": "システム", "xpack.aiAssistant.chatTimeline.messages.user.label": "あなた", "xpack.aiAssistant.checkingKbAvailability": "ナレッジベースの利用可能性を確認中", - "xpack.aiAssistant.conversationList.deleteConversationIconLabel": "削除", - "xpack.aiAssistant.conversationList.errorMessage": "の読み込みに失敗しました", - "xpack.aiAssistant.conversationList.noConversations": "会話なし", - "xpack.aiAssistant.conversationList.dateGroupTitle.today": "今日", - "xpack.aiAssistant.conversationList.dateGroupTitle.yesterday": "昨日", - "xpack.aiAssistant.conversationList.dateGroupTitle.thisWeek": "今週", + "xpack.aiAssistant.conversationList.dateGroupTitle.lastMonth": "先月", "xpack.aiAssistant.conversationList.dateGroupTitle.lastWeek": "先週", + "xpack.aiAssistant.conversationList.dateGroupTitle.older": "以前", "xpack.aiAssistant.conversationList.dateGroupTitle.thisMonth": "今月", - "xpack.aiAssistant.conversationList.dateGroupTitle.lastMonth": "先月", + "xpack.aiAssistant.conversationList.dateGroupTitle.thisWeek": "今週", "xpack.aiAssistant.conversationList.dateGroupTitle.thisYear": "今年", - "xpack.aiAssistant.conversationList.dateGroupTitle.older": "以前", + "xpack.aiAssistant.conversationList.dateGroupTitle.today": "今日", + "xpack.aiAssistant.conversationList.dateGroupTitle.yesterday": "昨日", + "xpack.aiAssistant.conversationList.deleteConversationIconLabel": "削除", + "xpack.aiAssistant.conversationList.errorMessage": "の読み込みに失敗しました", + "xpack.aiAssistant.conversationList.noConversations": "会話なし", "xpack.aiAssistant.conversationStartTitle": "会話を開始しました", "xpack.aiAssistant.couldNotFindConversationContent": "id {conversationId}の会話が見つかりませんでした。会話が存在し、それにアクセスできることを確認してください。", "xpack.aiAssistant.couldNotFindConversationTitle": "会話が見つかりません", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index fe24125547127..b6f26f5e452ac 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -9485,17 +9485,17 @@ "xpack.aiAssistant.chatTimeline.messages.system.label": "系统", "xpack.aiAssistant.chatTimeline.messages.user.label": "您", "xpack.aiAssistant.checkingKbAvailability": "正在检查知识库的可用性", - "xpack.aiAssistant.conversationList.deleteConversationIconLabel": "删除", - "xpack.aiAssistant.conversationList.errorMessage": "无法加载", - "xpack.aiAssistant.conversationList.noConversations": "无对话", - "xpack.aiAssistant.conversationList.dateGroupTitle.today": "今天", - "xpack.aiAssistant.conversationList.dateGroupTitle.yesterday": "昨天", - "xpack.aiAssistant.conversationList.dateGroupTitle.thisWeek": "本周", + "xpack.aiAssistant.conversationList.dateGroupTitle.lastMonth": "上月", "xpack.aiAssistant.conversationList.dateGroupTitle.lastWeek": "上周", + "xpack.aiAssistant.conversationList.dateGroupTitle.older": "更早", "xpack.aiAssistant.conversationList.dateGroupTitle.thisMonth": "本月", - "xpack.aiAssistant.conversationList.dateGroupTitle.lastMonth": "上月", + "xpack.aiAssistant.conversationList.dateGroupTitle.thisWeek": "本周", "xpack.aiAssistant.conversationList.dateGroupTitle.thisYear": "今年", - "xpack.aiAssistant.conversationList.dateGroupTitle.older": "更早", + "xpack.aiAssistant.conversationList.dateGroupTitle.today": "今天", + "xpack.aiAssistant.conversationList.dateGroupTitle.yesterday": "昨天", + "xpack.aiAssistant.conversationList.deleteConversationIconLabel": "删除", + "xpack.aiAssistant.conversationList.errorMessage": "无法加载", + "xpack.aiAssistant.conversationList.noConversations": "无对话", "xpack.aiAssistant.conversationStartTitle": "已开始对话", "xpack.aiAssistant.couldNotFindConversationContent": "找不到 ID 为 {conversationId} 的对话。请确保该对话存在并且您具有访问权限。", "xpack.aiAssistant.couldNotFindConversationTitle": "未找到对话", diff --git a/x-pack/platform/plugins/shared/automatic_import/public/components/create_integration/create_integration_landing/automatic_import_card.tsx b/x-pack/platform/plugins/shared/automatic_import/public/components/create_integration/create_integration_landing/automatic_import_card.tsx index e3278dab259d3..23a6c78b283ba 100644 --- a/x-pack/platform/plugins/shared/automatic_import/public/components/create_integration/create_integration_landing/automatic_import_card.tsx +++ b/x-pack/platform/plugins/shared/automatic_import/public/components/create_integration/create_integration_landing/automatic_import_card.tsx @@ -6,15 +6,7 @@ */ import React from 'react'; -import { - EuiButton, - EuiPanel, - EuiFlexGroup, - EuiFlexItem, - EuiText, - EuiTitle, - EuiBetaBadge, -} from '@elastic/eui'; +import { EuiButton, EuiPanel, EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle } from '@elastic/eui'; import { AssistantIcon } from '@kbn/ai-assistant-icon'; import { useAuthorization } from '../../../common/hooks/use_authorization'; import { MissingPrivilegesTooltip } from '../../../common/components/authorization'; @@ -44,16 +36,6 @@ export const AutomaticImportCard = React.memo(() => {

{i18n.AUTOMATIC_IMPORT_TITLE}

- - - diff --git a/x-pack/platform/plugins/shared/automatic_import/public/components/create_integration/create_integration_landing/translations.ts b/x-pack/platform/plugins/shared/automatic_import/public/components/create_integration/create_integration_landing/translations.ts index 468c6d6701e9e..7b83191ea69a1 100644 --- a/x-pack/platform/plugins/shared/automatic_import/public/components/create_integration/create_integration_landing/translations.ts +++ b/x-pack/platform/plugins/shared/automatic_import/public/components/create_integration/create_integration_landing/translations.ts @@ -63,18 +63,3 @@ export const AUTOMATIC_IMPORT_BUTTON = i18n.translate( defaultMessage: 'Create Integration', } ); - -export const TECH_PREVIEW = i18n.translate( - 'xpack.automaticImport.createIntegrationLanding.assistant.techPreviewBadge', - { - defaultMessage: 'Technical preview', - } -); - -export const TECH_PREVIEW_TOOLTIP = i18n.translate( - 'xpack.automaticImport.createIntegrationLanding.assistant.techPreviewTooltip', - { - defaultMessage: - 'This functionality is in technical preview and is subject to change. Please use with caution in production environments.', - } -); diff --git a/x-pack/platform/plugins/shared/fleet/cypress/e2e/integrations_automatic_import.cy.ts b/x-pack/platform/plugins/shared/fleet/cypress/e2e/integrations_automatic_import.cy.ts index 9175f0a04cab7..a4cb5a5980d44 100644 --- a/x-pack/platform/plugins/shared/fleet/cypress/e2e/integrations_automatic_import.cy.ts +++ b/x-pack/platform/plugins/shared/fleet/cypress/e2e/integrations_automatic_import.cy.ts @@ -9,7 +9,6 @@ import { deleteIntegrations } from '../tasks/integrations'; import { UPLOAD_PACKAGE_LINK, ASSISTANT_BUTTON, - TECH_PREVIEW_BADGE, CREATE_INTEGRATION_LANDING_PAGE, BUTTON_FOOTER_NEXT, INTEGRATION_TITLE_INPUT, @@ -77,7 +76,6 @@ describe('Add Integration - Automatic Import', () => { cy.getBySel(ASSISTANT_BUTTON).should('exist'); cy.getBySel(UPLOAD_PACKAGE_LINK).should('exist'); - cy.getBySel(TECH_PREVIEW_BADGE).should('exist'); // Create Automatic Import Page cy.getBySel(ASSISTANT_BUTTON).click(); diff --git a/x-pack/platform/plugins/shared/fleet/cypress/screens/integrations_automatic_import.ts b/x-pack/platform/plugins/shared/fleet/cypress/screens/integrations_automatic_import.ts index ab51e920708d7..363592f23d17e 100644 --- a/x-pack/platform/plugins/shared/fleet/cypress/screens/integrations_automatic_import.ts +++ b/x-pack/platform/plugins/shared/fleet/cypress/screens/integrations_automatic_import.ts @@ -7,7 +7,6 @@ export const UPLOAD_PACKAGE_LINK = 'uploadPackageLink'; export const ASSISTANT_BUTTON = 'assistantButton'; -export const TECH_PREVIEW_BADGE = 'techPreviewBadge'; export const MISSING_PRIVILEGES = 'missingPrivilegesCallOut'; export const CONNECTOR_BEDROCK = 'actionType-.bedrock'; diff --git a/x-pack/platform/plugins/shared/fleet/public/plugin.ts b/x-pack/platform/plugins/shared/fleet/public/plugin.ts index 993678b6ca649..9f4d42e113d3c 100644 --- a/x-pack/platform/plugins/shared/fleet/public/plugin.ts +++ b/x-pack/platform/plugins/shared/fleet/public/plugin.ts @@ -53,6 +53,8 @@ import type { DashboardStart } from '@kbn/dashboard-plugin/public'; import { Subject } from 'rxjs'; +import type { AutomaticImportPluginStart } from '@kbn/automatic-import-plugin/public'; + import type { FleetAuthz } from '../common'; import { appRoutesService, INTEGRATIONS_PLUGIN_ID, PLUGIN_ID, setupRouteService } from '../common'; import { @@ -87,7 +89,6 @@ import type { import { LazyCustomLogsAssetsExtension } from './lazy_custom_logs_assets_extension'; import { setCustomIntegrations, setCustomIntegrationsStart } from './services/custom_integrations'; import { getFleetDeepLinks } from './deep_links'; -import type { AutomaticImportPluginStart } from '@kbn/automatic-import-plugin/public'; export type { FleetConfigType } from '../common/types'; From 201169a04ae28133bbf8e1e7987b95de8673e368 Mon Sep 17 00:00:00 2001 From: "Quynh Nguyen (Quinn)" <43350163+qn895@users.noreply.github.com> Date: Wed, 29 Jan 2025 14:45:31 -0600 Subject: [PATCH 12/12] [ML] Add new View job detail flyouts for Anomaly detection and Data Frame Analytics (#207141) ## Summary This PR adds new View job detail flyout for Anomaly detection and Data Frame Analytics **For Anomaly detection jobs:** - New options are added when clicking on job's name (Remove from page, View datafeed charts, Navigate to Single Metric Viewer/Anomaly Explorer) Screenshot 2025-01-24 at 15 02 10 - If there's only one job, the remove from {page} is disabled Screenshot 2025-01-24 at 15 02 01 https://github.com/user-attachments/assets/1a4f0e8f-da15-4e8c-86bd-48045f9144f9 **For Anomaly detection groups:** - Remove job option is not shown https://github.com/user-attachments/assets/1976f7dc-8cfe-4f94-975e-233f0225e15b https://github.com/user-attachments/assets/3381a4f2-ec99-4848-b2fe-9df456306523 **For Data frame analytics jobs:** https://github.com/user-attachments/assets/7e067ac2-4eda-44b3-bc63-a5901912350f ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) ### Identify risks Does this PR introduce any risks? For example, consider risks like hard to test bugs, performance regression, potential of data loss. Describe the risk, its severity, and mitigation for each identified risk. Invite stakeholders and evaluate how to proceed before merging. - [ ] [See some risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) - [ ] ... --------- Co-authored-by: Elastic Machine --- .../get_options_for_job_selector_menu.tsx | 150 ++++++++++++++++++ .../group_selector_menu.tsx | 141 ++++++++++++++++ .../job_selector_button.tsx | 116 ++++++++++++++ .../job_selector/id_badges/id_badges.test.tsx | 26 +++ .../job_selector/id_badges/id_badges.tsx | 40 ++++- .../components/job_selector/job_selector.tsx | 34 +++- .../job_selector/job_selector_flyout.tsx | 2 +- .../new_selection_id_badges.test.tsx | 11 ++ .../analytics_detail_flyout.tsx | 129 +++++++++++++++ .../analytics_detail_flyout/index.ts | 8 + .../pages/analytics_exploration/page.tsx | 87 +++++----- .../analytics_service/get_analytics.ts | 12 +- .../analytics_id_selector_controls.tsx | 144 ++++++++++++----- .../pages/job_map/page.tsx | 4 +- .../public/application/explorer/explorer.tsx | 1 + .../components/job_details_flyout/index.ts | 9 ++ .../job_details_context_manager.tsx | 32 ++++ .../job_details_flyout/job_details_flyout.tsx | 146 +++++++++++++++++ .../job_details_flyout_context.tsx | 72 +++++++++ .../datafeed_chart_flyout.tsx | 50 +++--- .../job_details/datafeed_preview_tab.tsx | 4 +- .../components/job_details/job_details.js | 131 +++++++-------- .../memory_usage/memory_tree_map/tree_map.tsx | 1 - 23 files changed, 1162 insertions(+), 188 deletions(-) create mode 100644 x-pack/platform/plugins/shared/ml/public/application/components/job_selector/group_or_job_selector_menu/get_options_for_job_selector_menu.tsx create mode 100644 x-pack/platform/plugins/shared/ml/public/application/components/job_selector/group_or_job_selector_menu/group_selector_menu.tsx create mode 100644 x-pack/platform/plugins/shared/ml/public/application/components/job_selector/group_or_job_selector_menu/job_selector_button.tsx create mode 100644 x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/analytics_detail_flyout/analytics_detail_flyout.tsx create mode 100644 x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/analytics_detail_flyout/index.ts create mode 100644 x-pack/platform/plugins/shared/ml/public/application/jobs/components/job_details_flyout/index.ts create mode 100644 x-pack/platform/plugins/shared/ml/public/application/jobs/components/job_details_flyout/job_details_context_manager.tsx create mode 100644 x-pack/platform/plugins/shared/ml/public/application/jobs/components/job_details_flyout/job_details_flyout.tsx create mode 100644 x-pack/platform/plugins/shared/ml/public/application/jobs/components/job_details_flyout/job_details_flyout_context.tsx diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/group_or_job_selector_menu/get_options_for_job_selector_menu.tsx b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/group_or_job_selector_menu/get_options_for_job_selector_menu.tsx new file mode 100644 index 0000000000000..1727286ad1302 --- /dev/null +++ b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/group_or_job_selector_menu/get_options_for_job_selector_menu.tsx @@ -0,0 +1,150 @@ +/* + * 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 type { EuiContextMenuPanelItemDescriptor } from '@elastic/eui'; +import type { SharePluginStart } from '@kbn/share-plugin/public'; +import { ML_PAGES, type MlPages } from '../../../../locator'; +import { FlyoutType } from '../../../jobs/components/job_details_flyout/job_details_flyout_context'; +import { ML_APP_LOCATOR } from '../../../../../common/constants/locator'; + +const ANOMALY_EXPLORER_TITLE = i18n.translate('xpack.ml.anomalyExplorerPageLabel', { + defaultMessage: 'Anomaly Explorer', +}); +const SINGLE_METRIC_VIEWER_TITLE = i18n.translate('xpack.ml.singleMetricViewerPageLabel', { + defaultMessage: 'Single Metric Viewer', +}); + +export const getOptionsForJobSelectorMenuItems = ({ + jobId, + page, + onRemoveJobId, + removeJobIdDisabled, + showRemoveJobId, + isSingleMetricViewerDisabled, + closePopover, + globalState, + setActiveFlyout, + setActiveJobId, + navigateToUrl, + share, +}: { + jobId: string; + page: MlPages; + onRemoveJobId: (jobOrGroupId: string[]) => void; + removeJobIdDisabled: boolean; + showRemoveJobId: boolean; + isSingleMetricViewerDisabled: boolean; + closePopover: () => void; + globalState: Record; + setActiveFlyout: (flyout: FlyoutType | null) => void; + setActiveJobId: (jobId: string | null) => void; + navigateToUrl: (url: string) => void; + share: SharePluginStart; +}) => { + const pageToNavigateTo = + page === ML_PAGES.SINGLE_METRIC_VIEWER ? ANOMALY_EXPLORER_TITLE : SINGLE_METRIC_VIEWER_TITLE; + const viewInMenu: EuiContextMenuPanelItemDescriptor = { + name: i18n.translate( + 'xpack.ml.overview.anomalyDetection.jobContextMenu.viewInSingleMetricViewer', + { + defaultMessage: 'View in {page}', + values: { + page: pageToNavigateTo, + }, + } + ), + disabled: page === ML_PAGES.ANOMALY_EXPLORER && isSingleMetricViewerDisabled, + icon: 'visLine', + onClick: async () => { + const mlLocator = share.url.locators.get(ML_APP_LOCATOR); + if (!mlLocator) { + return; + } + const singleMetricViewerLink = await mlLocator.getUrl( + { + page: + page === ML_PAGES.SINGLE_METRIC_VIEWER + ? ML_PAGES.ANOMALY_EXPLORER + : ML_PAGES.SINGLE_METRIC_VIEWER, + pageState: { + globalState, + refreshInterval: { + display: 'Off', + pause: true, + value: 0, + }, + jobIds: [jobId], + query: { + query_string: { + analyze_wildcard: true, + query: '*', + }, + }, + }, + }, + { absolute: true } + ); + navigateToUrl(singleMetricViewerLink); + + closePopover(); + }, + }; + + const items = [ + { + name: i18n.translate('xpack.ml.overview.anomalyDetection.jobContextMenu.jobDetails', { + defaultMessage: 'Job details', + }), + icon: 'eye', + onClick: () => { + setActiveJobId(jobId); + setActiveFlyout(FlyoutType.JOB_DETAILS); + closePopover(); + }, + }, + ...(showRemoveJobId + ? [ + { + name: i18n.translate('xpack.ml.overview.anomalyDetection.jobContextMenu.removeJob', { + defaultMessage: 'Remove from {page}', + values: { + page: ANOMALY_EXPLORER_TITLE, + }, + }), + disabled: removeJobIdDisabled, + icon: 'minusInCircle', + onClick: () => { + if (onRemoveJobId) { + onRemoveJobId([jobId]); + setActiveJobId(jobId); + setActiveFlyout(null); + } + closePopover(); + }, + }, + ] + : []), + { + isSeparator: true, + key: 'separator2', + } as EuiContextMenuPanelItemDescriptor, + ...(viewInMenu.disabled ? [] : [viewInMenu]), + { + name: i18n.translate('xpack.ml.overview.anomalyDetection.jobContextMenu.viewDatafeedCounts', { + defaultMessage: 'View datafeed counts', + }), + icon: 'visAreaStacked', + onClick: () => { + setActiveJobId(jobId); + setActiveFlyout(FlyoutType.DATAFEED_CHART); + closePopover(); + }, + }, + ]; + return items; +}; diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/group_or_job_selector_menu/group_selector_menu.tsx b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/group_or_job_selector_menu/group_selector_menu.tsx new file mode 100644 index 0000000000000..265b94b129f39 --- /dev/null +++ b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/group_or_job_selector_menu/group_selector_menu.tsx @@ -0,0 +1,141 @@ +/* + * 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 { EuiContextMenuPanelDescriptor } from '@elastic/eui'; +import { EuiContextMenu, EuiNotificationBadge, EuiPopover } from '@elastic/eui'; + +import React, { useMemo, useState } from 'react'; +import { EuiButton } from '@elastic/eui'; +import { useUrlState } from '@kbn/ml-url-state'; +import { i18n } from '@kbn/i18n'; +import type { MlPages } from '../../../../locator'; +import { useJobInfoFlyouts } from '../../../jobs/components/job_details_flyout/job_details_flyout_context'; +import { getOptionsForJobSelectorMenuItems } from './get_options_for_job_selector_menu'; +import { useMlKibana } from '../../../contexts/kibana'; + +export const GroupSelectorMenu = ({ + groupId, + jobIds, + page, + onRemoveJobId, + removeJobIdDisabled, + removeGroupDisabled, + singleMetricViewerDisabledIds = [], +}: { + groupId: string; + jobIds: string[]; + page: MlPages; + onRemoveJobId: (jobOrGroupId: string[]) => void; + removeJobIdDisabled: boolean; + removeGroupDisabled: boolean; + singleMetricViewerDisabledIds: string[]; +}) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const { setActiveFlyout, setActiveJobId } = useJobInfoFlyouts(); + const { + services: { + share, + application: { navigateToUrl }, + }, + } = useMlKibana(); + const [globalState] = useUrlState('_g'); + + const closePopover = () => setIsPopoverOpen(false); + const onButtonClick = () => setIsPopoverOpen(true); + + const popoverId = `mlAnomalyGroupPopover-${groupId}`; + const button = ( + + {groupId} + {jobIds.length} + + ); + const panels: EuiContextMenuPanelDescriptor[] = useMemo(() => { + const items: EuiContextMenuPanelDescriptor[] = [ + { + id: 0, + items: [ + ...jobIds.map((jobId, idx) => ({ + panel: jobId, + name: jobId, + })), + { + isSeparator: true, + }, + { + name: i18n.translate('xpack.ml.groupSelectorMenu.removeGroupLabel', { + defaultMessage: 'Remove group from {page}', + values: { page }, + }), + icon: 'minusInCircle', + disabled: removeGroupDisabled, + onClick: () => { + onRemoveJobId([groupId, ...jobIds]); + closePopover(); + }, + }, + ], + }, + ]; + + jobIds.forEach((jobId) => { + const options = getOptionsForJobSelectorMenuItems({ + jobId, + page, + onRemoveJobId, + removeJobIdDisabled, + showRemoveJobId: false, + isSingleMetricViewerDisabled: singleMetricViewerDisabledIds.includes(jobId), + closePopover, + globalState, + setActiveFlyout, + setActiveJobId, + navigateToUrl, + share, + }); + const jobPanel: EuiContextMenuPanelDescriptor = { + id: jobId, + title: jobId, + items: options, + }; + items.push(jobPanel); + }); + return items; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + jobIds, + setActiveFlyout, + setActiveJobId, + // eslint-disable-next-line react-hooks/exhaustive-deps + JSON.stringify(globalState), + navigateToUrl, + share, + page, + onRemoveJobId, + removeJobIdDisabled, + removeGroupDisabled, + ]); + return ( + + + + ); +}; diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/group_or_job_selector_menu/job_selector_button.tsx b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/group_or_job_selector_menu/job_selector_button.tsx new file mode 100644 index 0000000000000..72929432db3ad --- /dev/null +++ b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/group_or_job_selector_menu/job_selector_button.tsx @@ -0,0 +1,116 @@ +/* + * 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 { FC } from 'react'; +import React, { useMemo, useState } from 'react'; +import type { EuiContextMenuPanelDescriptor } from '@elastic/eui'; +import { EuiButton, EuiContextMenu, EuiPopover, useGeneratedHtmlId } from '@elastic/eui'; +import { useUrlState } from '@kbn/ml-url-state'; +import { ML_PAGES, type MlPages } from '../../../../../common/constants/locator'; +import { useJobInfoFlyouts } from '../../../jobs/components/job_details_flyout'; +import { useMlKibana } from '../../../contexts/kibana'; +import { getOptionsForJobSelectorMenuItems } from './get_options_for_job_selector_menu'; + +interface Props { + jobId: string; + page: MlPages; + onRemoveJobId: (jobOrGroupId: string[]) => void; + removeJobIdDisabled: boolean; + isSingleMetricViewerDisabled: boolean; +} + +export const AnomalyDetectionInfoButton: FC = ({ + jobId, + page, + onRemoveJobId, + removeJobIdDisabled, + isSingleMetricViewerDisabled, +}) => { + const [isPopoverOpen, setPopover] = useState(false); + const { + services: { + share, + application: { navigateToUrl }, + }, + } = useMlKibana(); + const [globalState] = useUrlState('_g'); + + const popoverId = useGeneratedHtmlId({ + prefix: 'adJobInfoContextMenu', + suffix: jobId, + }); + const onButtonClick = () => { + setPopover(!isPopoverOpen); + }; + const closePopover = () => { + setPopover(false); + }; + + const { setActiveFlyout, setActiveJobId } = useJobInfoFlyouts(); + const panels = useMemo( + () => { + return [ + { + id: 0, + items: getOptionsForJobSelectorMenuItems({ + jobId, + page, + onRemoveJobId, + removeJobIdDisabled, + showRemoveJobId: page === ML_PAGES.ANOMALY_EXPLORER, + isSingleMetricViewerDisabled, + closePopover, + globalState, + setActiveFlyout, + setActiveJobId, + navigateToUrl, + share, + }), + }, + ] as EuiContextMenuPanelDescriptor[]; + }, + // globalState is an object with references change on every render, so we are stringifying it here + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + jobId, + page, + setActiveJobId, + setActiveFlyout, + navigateToUrl, + share.url.locators, + removeJobIdDisabled, + onRemoveJobId, + // eslint-disable-next-line react-hooks/exhaustive-deps + JSON.stringify(globalState), + ] + ); + + const button = ( + + {jobId} + + ); + return ( + + + + ); +}; diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/id_badges/id_badges.test.tsx b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/id_badges/id_badges.test.tsx index 424c3d8231863..bf530cde29e09 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/id_badges/id_badges.test.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/id_badges/id_badges.test.tsx @@ -9,8 +9,19 @@ import React from 'react'; import { render } from '@testing-library/react'; import type { IdBadgesProps } from './id_badges'; import { IdBadges } from './id_badges'; +import type { MlSummaryJob } from '../../../../../common/types/anomaly_detection_jobs'; + +jest.mock('../../../contexts/kibana', () => ({ + useMlKibana: () => ({ + services: { + share: { url: { locators: jest.fn() } }, + application: { navigateToUrl: jest.fn() }, + }, + }), +})); const props: IdBadgesProps = { + page: 'jobs', limit: 2, selectedGroups: [ { @@ -25,6 +36,21 @@ const props: IdBadgesProps = { selectedJobIds: ['job1', 'job2', 'job3'], onLinkClick: jest.fn(), showAllBarBadges: false, + onRemoveJobId: jest.fn(), + selectedJobs: [ + { + id: 'job1', + isSingleMetricViewerJob: false, + } as MlSummaryJob, + { + id: 'job2', + isSingleMetricViewerJob: true, + } as MlSummaryJob, + { + id: 'job3', + isSingleMetricViewerJob: true, + } as MlSummaryJob, + ], }; const overLimitProps: IdBadgesProps = { ...props, selectedJobIds: ['job4'] }; diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/id_badges/id_badges.tsx b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/id_badges/id_badges.tsx index b03ece5aaac55..20d396dc6b553 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/id_badges/id_badges.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/id_badges/id_badges.tsx @@ -5,11 +5,14 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { EuiFlexItem, EuiLink, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { JobSelectorBadge } from '../job_selector_badge'; +import { GroupSelectorMenu } from '../group_or_job_selector_menu/group_selector_menu'; import type { GroupObj } from '../job_selector'; +import { AnomalyDetectionInfoButton } from '../group_or_job_selector_menu/job_selector_button'; +import type { MlPages } from '../../../../../common/constants/locator'; +import type { MlSummaryJob } from '../../../../../common/types/anomaly_detection_jobs'; export interface IdBadgesProps { limit: number; @@ -17,6 +20,9 @@ export interface IdBadgesProps { selectedJobIds: string[]; onLinkClick: () => void; showAllBarBadges: boolean; + page: MlPages; + onRemoveJobId: (jobOrGroupId: string[]) => void; + selectedJobs: MlSummaryJob[]; } export function IdBadges({ @@ -25,18 +31,30 @@ export function IdBadges({ onLinkClick, selectedJobIds, showAllBarBadges, + page, + onRemoveJobId, + selectedJobs, }: IdBadgesProps) { const badges = []; - + const singleMetricViewerDisabledIds: string[] = useMemo( + () => selectedJobs.filter((job) => !job.isSingleMetricViewerJob).map((job) => job.id), + [selectedJobs] + ); // Create group badges. Skip job ids here. for (let i = 0; i < selectedGroups.length; i++) { const currentGroup = selectedGroups[i]; badges.push( - ); @@ -49,7 +67,13 @@ export function IdBadges({ } badges.push( - + ); } diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/job_selector.tsx b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/job_selector.tsx index 848da20c66e65..a01157b362c41 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/job_selector.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/job_selector.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { EuiButtonEmpty, @@ -24,9 +24,14 @@ import type { Dictionary } from '../../../../common/types/common'; import { IdBadges } from './id_badges'; import type { JobSelectorFlyoutProps } from './job_selector_flyout'; import { BADGE_LIMIT, JobSelectorFlyoutContent } from './job_selector_flyout'; -import type { MlJobWithTimeRange } from '../../../../common/types/anomaly_detection_jobs'; +import type { + MlJobWithTimeRange, + MlSummaryJob, +} from '../../../../common/types/anomaly_detection_jobs'; import { ML_APPLY_TIME_RANGE_CONFIG } from '../../../../common/types/storage'; import { FeedBackButton } from '../feedback_button'; +import { JobInfoFlyoutsProvider } from '../../jobs/components/job_details_flyout'; +import { JobInfoFlyoutsManager } from '../../jobs/components/job_details_flyout/job_details_context_manager'; export interface GroupObj { groupId: string; @@ -86,6 +91,7 @@ export interface JobSelectorProps { }) => void; selectedJobIds?: string[]; selectedGroups?: GroupObj[]; + selectedJobs?: MlSummaryJob[]; } export interface JobSelectionMaps { @@ -99,6 +105,7 @@ export function JobSelector({ timeseriesOnly, selectedJobIds = [], selectedGroups = [], + selectedJobs = [], onSelectionChange, }: JobSelectorProps) { const [applyTimeRangeConfig, setApplyTimeRangeConfig] = useStorage( @@ -141,6 +148,14 @@ export function JobSelector({ [onSelectionChange] ); + const page = useMemo(() => { + return singleSelection ? ML_PAGES.SINGLE_METRIC_VIEWER : ML_PAGES.ANOMALY_EXPLORER; + }, [singleSelection]); + + const removeJobId = (jobOrGroupId: string[]) => { + const newSelection = selectedIds.filter((id) => !jobOrGroupId.includes(id)); + applySelection({ newSelection, jobIds: newSelection, time: undefined }); + }; function renderJobSelectionBar() { return ( <> @@ -159,7 +174,10 @@ export function JobSelector({ onLinkClick={() => setShowAllBarBadges(!showAllBarBadges)} selectedJobIds={selectedJobIds} selectedGroups={selectedGroups} + selectedJobs={selectedJobs} showAllBarBadges={showAllBarBadges} + page={page} + onRemoveJobId={removeJobId} /> ) : ( @@ -187,10 +205,7 @@ export function JobSelector({ - + @@ -223,8 +238,11 @@ export function JobSelector({ return (
- {renderJobSelectionBar()} - {renderFlyout()} + + {renderJobSelectionBar()} + {renderFlyout()} + +
); } diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/job_selector_flyout.tsx b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/job_selector_flyout.tsx index 4684ef1e63b43..58948161af298 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/job_selector_flyout.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/job_selector_flyout.tsx @@ -42,7 +42,7 @@ export const DEFAULT_GANTT_BAR_WIDTH = 299; // pixels export interface JobSelectionResult { newSelection: string[]; jobIds: string[]; - time: { from: string; to: string } | undefined; + time?: { from: string; to: string } | undefined; } export interface JobSelectorFlyoutProps { diff --git a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.test.tsx b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.test.tsx index 101c6f53d33fd..192c6a6ef708d 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.test.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/components/job_selector/new_selection_id_badges/new_selection_id_badges.test.tsx @@ -10,6 +10,17 @@ import { render } from '@testing-library/react'; import type { NewSelectionIdBadgesProps } from './new_selection_id_badges'; import { NewSelectionIdBadges } from './new_selection_id_badges'; +jest.mock('../../../contexts/kibana', () => ({ + useMlKibana: () => ({ + services: { + share: {}, + application: { + navigateToUrl: jest.fn(), + }, + }, + }), +})); + const props: NewSelectionIdBadgesProps = { limit: 2, groups: [ diff --git a/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/analytics_detail_flyout/analytics_detail_flyout.tsx b/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/analytics_detail_flyout/analytics_detail_flyout.tsx new file mode 100644 index 0000000000000..80e8513f07ec3 --- /dev/null +++ b/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/analytics_detail_flyout/analytics_detail_flyout.tsx @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiLoadingSpinner, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useUrlState } from '@kbn/ml-url-state'; +import { useJobInfoFlyouts } from '../../../../../jobs/components/job_details_flyout'; +import { useGetAnalytics } from '../../../analytics_management/services/analytics_service'; +import type { AnalyticStatsBarStats } from '../../../../../components/stats_bar'; +import type { DataFrameAnalyticsListRow } from '../../../analytics_management/components/analytics_list/common'; +import { useMlLocator, useNavigateToPath } from '../../../../../contexts/kibana'; +import { ML_PAGES } from '../../../../../../locator'; +import { ExpandedRow } from '../../../analytics_management/components/analytics_list/expanded_row'; + +export const AnalyticsDetailFlyout = () => { + const { + isDataFrameAnalyticsDetailsFlyoutOpen, + closeActiveFlyout, + setActiveJobId, + activeJobId: analyticsId, + } = useJobInfoFlyouts(); + const [analytics, setAnalytics] = useState([]); + const [, setAnalyticsStats] = useState(undefined); + const [, setErrorMessage] = useState(undefined); + const [, setIsInitialized] = useState(false); + const [, setJobsAwaitingNodeCount] = useState(0); + const blockRefresh = false; + const getAnalytics = useGetAnalytics( + setAnalytics, + setAnalyticsStats, + setErrorMessage, + setIsInitialized, + setJobsAwaitingNodeCount, + blockRefresh + ); + const getAnalyticsCallback = useCallback( + (id: string | null) => getAnalytics(true, id), + [getAnalytics] + ); + + // Subscribe to the refresh observable to trigger reloading the analytics list. + + useEffect( + function getAnalyticsDetailsOnChange() { + if (!analyticsId) { + return; + } + getAnalyticsCallback(analyticsId); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [analyticsId] + ); + const analytic = useMemo( + () => analytics.find((a) => a.id === analyticsId), + [analytics, analyticsId] + ); + + const locator = useMlLocator()!; + const navigateToPath = useNavigateToPath(); + const [globalState] = useUrlState('_g'); + + const redirectToAnalyticsList = useCallback(async () => { + const path = await locator.getUrl({ + page: ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE, + pageState: { + jobId: globalState?.ml?.jobId, + }, + }); + await navigateToPath(path, false); + }, [locator, navigateToPath, globalState?.ml?.jobId]); + + const flyoutTitleId = `mlAnalyticsDetailsFlyout-${analyticsId}`; + return isDataFrameAnalyticsDetailsFlyoutOpen ? ( + { + closeActiveFlyout(); + setActiveJobId(null); + }} + aria-labelledby={flyoutTitleId} + > + + + + +

{analyticsId}

+
+
+ + + + + + +
+
+ + {analytic ? ( + + ) : ( + + + + )} + +
+ ) : null; +}; diff --git a/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/analytics_detail_flyout/index.ts b/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/analytics_detail_flyout/index.ts new file mode 100644 index 0000000000000..fb2a7dd731aed --- /dev/null +++ b/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/analytics_detail_flyout/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 { AnalyticsDetailFlyout } from './analytics_detail_flyout'; diff --git a/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx b/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx index bf4bc9e7db894..1fa214b98fdd7 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_exploration/page.tsx @@ -26,6 +26,8 @@ import type { AnalyticsSelectorIds } from '../components/analytics_selector'; import { AnalyticsIdSelector, AnalyticsIdSelectorControls } from '../components/analytics_selector'; import { AnalyticsEmptyPrompt } from '../analytics_management/components/empty_prompt'; import { SavedObjectsWarning } from '../../../components/saved_objects_warning'; +import { JobInfoFlyoutsProvider } from '../../../jobs/components/job_details_flyout/job_details_flyout_context'; +import { AnalyticsDetailFlyout } from './components/analytics_detail_flyout'; export const Page: FC<{ jobId: string; @@ -131,52 +133,55 @@ export const Page: FC<{ return ( <> - - {isIdSelectorFlyoutVisible ? ( - + + - ) : null} - {jobIdToUse !== undefined && ( - - - - )} - {jobIdToUse === undefined && ( - - - - )} + ) : null} + {jobIdToUse !== undefined && ( + + + + )} + {jobIdToUse === undefined && ( + + + + )} - + - {jobIdToUse && analysisTypeToUse ? ( -
- {analysisTypeToUse === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION && ( - - )} - {analysisTypeToUse === ANALYSIS_CONFIG_TYPE.REGRESSION && ( - - )} - {analysisTypeToUse === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && ( - - )} -
- ) : ( - getEmptyState() - )} - + {jobIdToUse && analysisTypeToUse ? ( +
+ {analysisTypeToUse === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION && ( + + )} + {analysisTypeToUse === ANALYSIS_CONFIG_TYPE.REGRESSION && ( + + )} + {analysisTypeToUse === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && ( + + )} +
+ ) : ( + getEmptyState() + )} + + ); }; diff --git a/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts b/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts index 6fc2951271fb6..b984cf1576bda 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts +++ b/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/get_analytics.ts @@ -41,7 +41,7 @@ export const isGetDataFrameAnalyticsStatsResponseOk = ( ); }; -export type GetAnalytics = (forceRefresh?: boolean) => void; +export type GetAnalytics = (forceRefresh?: boolean, nullableAnalyticsId?: string | null) => void; /** * Gets initial object for analytics stats. @@ -124,7 +124,7 @@ export const useGetAnalytics = ( let concurrentLoads = 0; - const getAnalytics = async (forceRefresh = false) => { + const getAnalytics = async (forceRefresh = false, nullableAnalyticsId?: string | null) => { if (forceRefresh === true || blockRefresh === false) { refreshAnalyticsList$.next(REFRESH_ANALYTICS_LIST_STATE.LOADING); concurrentLoads++; @@ -133,9 +133,13 @@ export const useGetAnalytics = ( return; } + const analyticsId = nullableAnalyticsId ?? undefined; + try { - const analyticsConfigs = await mlApi.dataFrameAnalytics.getDataFrameAnalytics(); - const analyticsStats = await mlApi.dataFrameAnalytics.getDataFrameAnalyticsStats(); + const analyticsConfigs = await mlApi.dataFrameAnalytics.getDataFrameAnalytics(analyticsId); + const analyticsStats = await mlApi.dataFrameAnalytics.getDataFrameAnalyticsStats( + analyticsId + ); let savedObjectsSpaces: Record = {}; if (canManageSpacesAndSavedObjects && mlApi.savedObjects.jobsSpaces) { diff --git a/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/components/analytics_selector/analytics_id_selector_controls.tsx b/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/components/analytics_selector/analytics_id_selector_controls.tsx index 4c73ff95fe74e..57037c54fcf0a 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/components/analytics_selector/analytics_id_selector_controls.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/components/analytics_selector/analytics_id_selector_controls.tsx @@ -6,61 +6,127 @@ */ import type { FC } from 'react'; -import React from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; +import type { EuiContextMenuPanelDescriptor } from '@elastic/eui'; import { - EuiBadge, + EuiButton, EuiButtonEmpty, + EuiContextMenu, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, + EuiPopover, EuiText, } from '@elastic/eui'; - +import { i18n } from '@kbn/i18n'; +import { + FlyoutType, + useJobInfoFlyouts, +} from '../../../../jobs/components/job_details_flyout/job_details_flyout_context'; interface Props { setIsIdSelectorFlyoutVisible: React.Dispatch>; selectedId?: string; } +interface SelectorControlProps { + analyticsId: string; + 'data-test-subj': string; +} + +const SelectorControl = ({ analyticsId, 'data-test-subj': dataTestSubj }: SelectorControlProps) => { + const { setActiveJobId, setActiveFlyout } = useJobInfoFlyouts(); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const closePopover = useCallback(() => { + setIsPopoverOpen(false); + }, []); + + const panels = useMemo(() => { + return [ + { + id: 0, + items: [ + { + name: i18n.translate('xpack.ml.overview.dataFrameAnalytics.jobContextMenu.details', { + defaultMessage: 'Job details', + }), + icon: 'eye', + onClick: () => { + setActiveJobId(analyticsId); + setActiveFlyout(FlyoutType.DATA_FRAME_ANALYTICS_DETAILS); + closePopover(); + }, + }, + ], + }, + ] as EuiContextMenuPanelDescriptor[]; + }, [analyticsId, closePopover, setActiveFlyout, setActiveJobId]); + + const button = ( + + {analyticsId} + + ); + return ( + + + + ); +}; + export const AnalyticsIdSelectorControls: FC = ({ setIsIdSelectorFlyoutVisible, selectedId, -}) => ( - <> - - - {selectedId ? ( - { + return ( + <> + + + {selectedId ? ( + + ) : null} + {!selectedId ? ( + + + + ) : null} + + + - {selectedId} - - ) : null} - {!selectedId ? ( - - - ) : null} - - - - - - - - - -); + +
+ + + + ); +}; diff --git a/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/job_map/page.tsx b/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/job_map/page.tsx index 2f1936a0c89de..c56dd07ec25eb 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/job_map/page.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/data_frame_analytics/pages/job_map/page.tsx @@ -34,7 +34,7 @@ export const Page: FC = () => { const [isLoadingJobsExist, setIsLoadingJobsExist] = useState(false); const { refresh } = useRefreshAnalyticsList({ isLoading: setIsLoading }); - const setAnalyticsId = useCallback( + const onAnalyticsIdChange = useCallback( (analyticsId: AnalyticsSelectorIds) => { setGlobalState({ ml: { @@ -108,7 +108,7 @@ export const Page: FC = () => { /> {isIdSelectorFlyoutVisible ? ( ) : null} diff --git a/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer.tsx b/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer.tsx index 0ffb38f3631d1..d5ae0f0ebf219 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/explorer/explorer.tsx @@ -427,6 +427,7 @@ export const Explorer: FC = ({ onSelectionChange: handleJobSelectionChange, selectedJobIds, selectedGroups, + selectedJobs, } as unknown as JobSelectorProps; const noJobsSelected = !selectedJobs || selectedJobs.length === 0; diff --git a/x-pack/platform/plugins/shared/ml/public/application/jobs/components/job_details_flyout/index.ts b/x-pack/platform/plugins/shared/ml/public/application/jobs/components/job_details_flyout/index.ts new file mode 100644 index 0000000000000..dae8398a71ce5 --- /dev/null +++ b/x-pack/platform/plugins/shared/ml/public/application/jobs/components/job_details_flyout/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { JobDetailsFlyout } from './job_details_flyout'; +export { JobInfoFlyoutsProvider, useJobInfoFlyouts } from './job_details_flyout_context'; diff --git a/x-pack/platform/plugins/shared/ml/public/application/jobs/components/job_details_flyout/job_details_context_manager.tsx b/x-pack/platform/plugins/shared/ml/public/application/jobs/components/job_details_flyout/job_details_context_manager.tsx new file mode 100644 index 0000000000000..6e599bfcfc2d6 --- /dev/null +++ b/x-pack/platform/plugins/shared/ml/public/application/jobs/components/job_details_flyout/job_details_context_manager.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useContext, useMemo } from 'react'; +import { useUrlState } from '@kbn/ml-url-state'; +import moment from 'moment'; +import { JobDetailsFlyout } from './job_details_flyout'; +import { DatafeedChartFlyout } from '../../jobs_list/components/datafeed_chart_flyout'; +import { JobInfoFlyoutsContext } from './job_details_flyout_context'; + +export const JobInfoFlyoutsManager = () => { + const { isDatafeedChartFlyoutOpen, activeJobId, closeActiveFlyout } = + useContext(JobInfoFlyoutsContext); + const [globalState] = useUrlState('_g'); + const end = useMemo( + () => moment(globalState?.time?.to).unix() * 1000 ?? 0, + [globalState?.time?.to] + ); + + return ( + <> + + {isDatafeedChartFlyoutOpen && activeJobId ? ( + + ) : null} + + ); +}; diff --git a/x-pack/platform/plugins/shared/ml/public/application/jobs/components/job_details_flyout/job_details_flyout.tsx b/x-pack/platform/plugins/shared/ml/public/application/jobs/components/job_details_flyout/job_details_flyout.tsx new file mode 100644 index 0000000000000..bbc4e064b737f --- /dev/null +++ b/x-pack/platform/plugins/shared/ml/public/application/jobs/components/job_details_flyout/job_details_flyout.tsx @@ -0,0 +1,146 @@ +/* + * 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, { useEffect, useState } from 'react'; +import { + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiText, + EuiTitle, + EuiFlexItem, + EuiButtonEmpty, + EuiFlexGroup, + useGeneratedHtmlId, + EuiLoadingSpinner, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import useMountedState from 'react-use/lib/useMountedState'; +import type { CombinedJobWithStats } from '../../../../../common/types/anomaly_detection_jobs'; +import { useMlApi, useMlLocator, useNavigateToPath } from '../../../contexts/kibana'; +import { JobDetails } from '../../jobs_list/components/job_details'; +import { loadFullJob } from '../../jobs_list/components/utils'; +import { useToastNotificationService } from '../../../services/toast_notification_service'; +import { ML_PAGES } from '../../../../../common/constants/locator'; +import { useJobInfoFlyouts } from './job_details_flyout_context'; + +const doNothing = () => {}; +export const JobDetailsFlyout = () => { + const { + isDetailFlyoutOpen, + activeJobId: jobId, + setActiveJobId, + closeActiveFlyout, + } = useJobInfoFlyouts(); + const flyoutTitleId = useGeneratedHtmlId({ + prefix: 'jobDetailsFlyout', + }); + const [isLoading, setIsLoading] = useState(false); + const [jobDetails, setJobDetails] = useState(null); + const mlApi = useMlApi(); + const { displayErrorToast } = useToastNotificationService(); + + const isMounted = useMountedState(); + useEffect(() => { + const fetchJobDetails = async () => { + if (!isMounted()) return; + if (jobId) { + setIsLoading(true); + try { + const job = await loadFullJob(mlApi, jobId); + if (job) { + setJobDetails(job); + } + } catch (error) { + displayErrorToast( + error, + i18n.translate('xpack.ml.jobDetailsFlyout.errorFetchingJobDetails', { + defaultMessage: 'Error fetching job details', + }) + ); + } finally { + setIsLoading(false); + } + } + }; + fetchJobDetails(); + }, [jobId, mlApi, displayErrorToast, isMounted]); + + const navigateToPath = useNavigateToPath(); + const mlLocator = useMlLocator(); + + if (!jobId) { + return null; + } + + const openJobsList = async () => { + const pageState = { jobId }; + if (mlLocator) { + const url = await mlLocator.getUrl({ + page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, + pageState, + }); + await navigateToPath(url); + } + }; + + return isDetailFlyoutOpen ? ( + { + closeActiveFlyout(); + setActiveJobId(null); + }} + aria-labelledby={flyoutTitleId} + > + + + + +

{jobId}

+
+
+ + + + + + +
+
+ + {isLoading ? ( + + + + ) : ( + + {jobDetails ? ( + + ) : null} + + )} + +
+ ) : null; +}; diff --git a/x-pack/platform/plugins/shared/ml/public/application/jobs/components/job_details_flyout/job_details_flyout_context.tsx b/x-pack/platform/plugins/shared/ml/public/application/jobs/components/job_details_flyout/job_details_flyout_context.tsx new file mode 100644 index 0000000000000..29dc9522ea8de --- /dev/null +++ b/x-pack/platform/plugins/shared/ml/public/application/jobs/components/job_details_flyout/job_details_flyout_context.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { createContext, useContext, useMemo, useState, useCallback } from 'react'; + +export enum FlyoutType { + JOB_DETAILS = 'jobDetails', + DATAFEED_CHART = 'datafeedChart', + DATA_FRAME_ANALYTICS_DETAILS = 'dataFrameAnalyticsDetails', +} +interface JobInfoFlyoutsContextValue { + activeJobId: string | null; + setActiveJobId: (jobId: string | null) => void; + activeFlyout: FlyoutType | null; + setActiveFlyout: (flyout: FlyoutType | null) => void; + isDetailFlyoutOpen: boolean; + isDatafeedChartFlyoutOpen: boolean; + closeActiveFlyout: () => void; + isDataFrameAnalyticsDetailsFlyoutOpen: boolean; +} + +export const JobInfoFlyoutsContext = createContext({ + activeJobId: null, + setActiveJobId: () => {}, + activeFlyout: null, + setActiveFlyout: () => {}, + isDetailFlyoutOpen: false, + isDatafeedChartFlyoutOpen: false, + closeActiveFlyout: () => {}, + isDataFrameAnalyticsDetailsFlyoutOpen: false, +}); + +export const useJobInfoFlyouts = () => useContext(JobInfoFlyoutsContext); + +export const JobInfoFlyoutsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [activeJobId, setActiveJobId] = useState(null); + const [activeFlyout, setActiveFlyout] = useState(null); + const isDetailFlyoutOpen = useMemo(() => activeFlyout === FlyoutType.JOB_DETAILS, [activeFlyout]); + const isDatafeedChartFlyoutOpen = useMemo( + () => activeFlyout === FlyoutType.DATAFEED_CHART, + [activeFlyout] + ); + const isDataFrameAnalyticsDetailsFlyoutOpen = useMemo( + () => activeFlyout === FlyoutType.DATA_FRAME_ANALYTICS_DETAILS, + [activeFlyout] + ); + const closeActiveFlyout = useCallback(() => { + setActiveJobId(null); + setActiveFlyout(null); + }, [setActiveJobId, setActiveFlyout]); + + return ( + + {children} + + ); +}; diff --git a/x-pack/platform/plugins/shared/ml/public/application/jobs/jobs_list/components/datafeed_chart_flyout/datafeed_chart_flyout.tsx b/x-pack/platform/plugins/shared/ml/public/application/jobs/jobs_list/components/datafeed_chart_flyout/datafeed_chart_flyout.tsx index bc7b4b4d3a2fb..554ed0163f8ef 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/jobs/jobs_list/components/datafeed_chart_flyout/datafeed_chart_flyout.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/jobs/jobs_list/components/datafeed_chart_flyout/datafeed_chart_flyout.tsx @@ -52,6 +52,7 @@ import { Tooltip, TooltipType, } from '@elastic/charts'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { DATAFEED_STATE } from '../../../../../../common/constants/states'; import type { CombinedJobWithStats, @@ -87,7 +88,7 @@ interface DatafeedChartFlyoutProps { jobId: string; end: number; onClose: () => void; - onModelSnapshotAnnotationClick: (modelSnapshot: ModelSnapshot) => void; + onModelSnapshotAnnotationClick?: (modelSnapshot: ModelSnapshot) => void; } function setLineAnnotationHeader( @@ -356,21 +357,23 @@ export const DatafeedChartFlyout: FC = ({ onChange={() => setShowAnnotations(!showAnnotations)} />
- - - - - } - checked={showModelSnapshots} - onChange={() => setShowModelSnapshots(!showModelSnapshots)} - /> - + {onModelSnapshotAnnotationClick ? ( + + + + + } + checked={showModelSnapshots} + onChange={() => setShowModelSnapshots(!showModelSnapshots)} + /> + + ) : null} @@ -421,10 +424,17 @@ export const DatafeedChartFlyout: FC = ({ ) return; - onModelSnapshotAnnotationClick( - // @ts-expect-error property 'modelSnapshot' does not exist on type - annotations.lines[0].datum.modelSnapshot - ); + if ( + onModelSnapshotAnnotationClick && + isPopulatedObject( + annotations.lines?.[0]?.datum, + ['modelSnapshot'] + ) + ) { + onModelSnapshotAnnotationClick( + annotations.lines[0].datum.modelSnapshot + ); + } }} theme={{ lineSeriesStyle: { diff --git a/x-pack/platform/plugins/shared/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.tsx b/x-pack/platform/plugins/shared/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.tsx index 38ac41983f489..0475d128b98b3 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/jobs/jobs_list/components/job_details/datafeed_preview_tab.tsx @@ -52,7 +52,9 @@ export const DatafeedPreviewPane: FC = ({ job }) => { } return loading ? ( - +
+ +
) : ( <> {previewJson === null ? ( diff --git a/x-pack/platform/plugins/shared/ml/public/application/jobs/jobs_list/components/job_details/job_details.js b/x-pack/platform/plugins/shared/ml/public/application/jobs/jobs_list/components/job_details/job_details.js index 4eec95106eca2..6e6cab09cfd5c 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/jobs/jobs_list/components/job_details/job_details.js +++ b/x-pack/platform/plugins/shared/ml/public/application/jobs/jobs_list/components/job_details/job_details.js @@ -177,19 +177,23 @@ export class JobDetailsUI extends Component { ), }, - { - id: 'counts', - 'data-test-subj': 'mlJobListTab-counts', - name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.countsLabel', { - defaultMessage: 'Counts', - }), - content: ( - - ), - }, + ...(this.props.mode !== 'flyout' + ? [ + { + id: 'counts', + 'data-test-subj': 'mlJobListTab-counts', + name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.countsLabel', { + defaultMessage: 'Counts', + }), + content: ( + + ), + }, + ] + : []), { id: 'json', 'data-test-subj': 'mlJobListTab-json', @@ -214,58 +218,59 @@ export class JobDetailsUI extends Component { }, ]; - if (datafeed.items.length) { - tabs.push( - { - id: 'datafeed-preview', - disabled: job.blocked !== undefined, - 'data-test-subj': 'mlJobListTab-datafeed-preview', - name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.datafeedPreviewLabel', { - defaultMessage: 'Datafeed preview', - }), - content: , - }, - { - id: 'forecasts', - disabled: job.blocked !== undefined, - 'data-test-subj': 'mlJobListTab-forecasts', - name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.forecastsLabel', { - defaultMessage: 'Forecasts', - }), - content: , - } - ); - } - - tabs.push({ - id: 'annotations', - disabled: job.blocked !== undefined, - 'data-test-subj': 'mlJobListTab-annotations', - name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.annotationsLabel', { - defaultMessage: 'Annotations', - }), - content: ( - - - - - ), - }); + if (this.props.mode !== 'flyout') { + if (datafeed.items.length) { + tabs.push( + { + id: 'datafeed-preview', + disabled: job.blocked !== undefined, + 'data-test-subj': 'mlJobListTab-datafeed-preview', + name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.datafeedPreviewLabel', { + defaultMessage: 'Datafeed preview', + }), + content: , + }, + { + id: 'forecasts', + disabled: job.blocked !== undefined, + 'data-test-subj': 'mlJobListTab-forecasts', + name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.forecastsLabel', { + defaultMessage: 'Forecasts', + }), + content: , + } + ); + } - tabs.push({ - id: 'modelSnapshots', - disabled: job.blocked !== undefined, - 'data-test-subj': 'mlJobListTab-modelSnapshots', - name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.modelSnapshotsLabel', { - defaultMessage: 'Model snapshots', - }), - content: ( - - - - ), - }); + tabs.push({ + id: 'annotations', + disabled: job.blocked !== undefined, + 'data-test-subj': 'mlJobListTab-annotations', + name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.annotationsLabel', { + defaultMessage: 'Annotations', + }), + content: ( + + + + + ), + }); + tabs.push({ + id: 'modelSnapshots', + disabled: job.blocked !== undefined, + 'data-test-subj': 'mlJobListTab-modelSnapshots', + name: i18n.translate('xpack.ml.jobsList.jobDetails.tabs.modelSnapshotsLabel', { + defaultMessage: 'Model snapshots', + }), + content: ( + + + + ), + }); + } return (
{}} /> diff --git a/x-pack/platform/plugins/shared/ml/public/application/memory_usage/memory_tree_map/tree_map.tsx b/x-pack/platform/plugins/shared/ml/public/application/memory_usage/memory_tree_map/tree_map.tsx index 5ae83fcba9ab7..e4ae01fe0ebc6 100644 --- a/x-pack/platform/plugins/shared/ml/public/application/memory_usage/memory_tree_map/tree_map.tsx +++ b/x-pack/platform/plugins/shared/ml/public/application/memory_usage/memory_tree_map/tree_map.tsx @@ -136,7 +136,6 @@ export const JobMemoryTreeMap: FC = ({ node, type, height }) => { }, [loadJobMemorySize, refresh] ); - return (