diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index e6eb1cec755c8..2e19b903cc8d3 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -149,7 +149,7 @@ enabled: - test/plugin_functional/config.ts - test/server_integration/http/platform/config.status.ts - test/server_integration/http/platform/config.ts - - test/server_integration/http/ssl_redirect/config.js + - test/server_integration/http/ssl_redirect/config.ts - test/server_integration/http/ssl_with_p12_intermediate/config.js - test/server_integration/http/ssl_with_p12/config.js - test/server_integration/http/ssl/config.js diff --git a/.ci/Dockerfile b/.ci/Dockerfile index 3165805fe68c1..bf84d0a78d581 100644 --- a/.ci/Dockerfile +++ b/.ci/Dockerfile @@ -1,7 +1,7 @@ # NOTE: This Dockerfile is ONLY used to run certain tasks in CI. It is not used to run Kibana or as a distributable. # If you're looking for the Kibana Docker image distributable, please see: src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts -ARG NODE_VERSION=18.17.0 +ARG NODE_VERSION=18.17.1 FROM node:${NODE_VERSION} AS base diff --git a/.eslintrc.js b/.eslintrc.js index 515fbef1f2e4e..ddd39ed00747a 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -596,6 +596,7 @@ module.exports = { 'test/*/config_open.ts', 'test/*/*.config.ts', 'test/*/{tests,test_suites,apis,apps}/**/*', + 'test/server_integration/**/*.ts', 'x-pack/test/*/{tests,test_suites,apis,apps}/**/*', 'x-pack/test/*/*config.*ts', 'x-pack/test/saved_object_api_integration/*/apis/**/*', diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index aa0442041059b..7f82bd7e8c41e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -9,7 +9,7 @@ x-pack/test/alerting_api_integration/common/plugins/aad @elastic/response-ops packages/kbn-ace @elastic/platform-deployment-management x-pack/plugins/actions @elastic/response-ops x-pack/test/alerting_api_integration/common/plugins/actions_simulators @elastic/response-ops -src/plugins/advanced_settings @elastic/appex-sharedux +src/plugins/advanced_settings @elastic/appex-sharedux @elastic/platform-deployment-management x-pack/packages/ml/aiops_components @elastic/ml-ui x-pack/plugins/aiops @elastic/ml-ui x-pack/packages/ml/aiops_utils @elastic/ml-ui @@ -478,6 +478,7 @@ packages/kbn-managed-vscode-config @elastic/kibana-operations packages/kbn-managed-vscode-config-cli @elastic/kibana-operations packages/kbn-management/cards_navigation @elastic/platform-deployment-management src/plugins/management @elastic/platform-deployment-management +packages/kbn-management/settings/section_registry @elastic/appex-sharedux @elastic/platform-deployment-management packages/kbn-management/storybook/config @elastic/platform-deployment-management test/plugin_functional/plugins/management_test_plugin @elastic/kibana-app-services packages/kbn-mapbox-gl @elastic/kibana-gis @@ -1197,6 +1198,8 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib /x-pack/plugins/security_solution/server/lib/detection_engine/rule_schema @elastic/security-detection-rule-management @elastic/security-detection-engine /x-pack/plugins/security_solution/server/utils @elastic/security-detection-rule-management +/x-pack/plugins/security_solution/scripts/openapi @elastic/security-detection-rule-management + ## Security Solution sub teams - Detection Engine /x-pack/plugins/security_solution/common/api/detection_engine @elastic/security-detection-engine diff --git a/.node-version b/.node-version index 603606bc91118..4a1f488b6c3b6 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -18.17.0 +18.17.1 diff --git a/.nvmrc b/.nvmrc index 603606bc91118..4a1f488b6c3b6 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18.17.0 +18.17.1 diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel index baec139453143..dd4c41818949c 100644 --- a/WORKSPACE.bazel +++ b/WORKSPACE.bazel @@ -22,13 +22,13 @@ load("@build_bazel_rules_nodejs//:index.bzl", "node_repositories", "yarn_install # Setup the Node.js toolchain for the architectures we want to support node_repositories( node_repositories = { - "18.17.0-darwin_amd64": ("node-v18.17.0-darwin-x64.tar.gz", "node-v18.17.0-darwin-x64", "2f381442381f7fbde2ca644c3275bec9c9c2a8d361f467b40e39428acdd6ccff"), - "18.17.0-darwin_arm64": ("node-v18.17.0-darwin-arm64.tar.gz", "node-v18.17.0-darwin-arm64", "19731ef427e77ad9c5f476eb62bfb02a7f179d3012feed0bbded62e45f23e679"), - "18.17.0-linux_arm64": ("node-v18.17.0-linux-arm64.tar.xz", "node-v18.17.0-linux-arm64", "fbd2904178ee47da6e0386bc9704a12b1f613da6ad194878a517d4a69ba56544"), - "18.17.0-linux_amd64": ("node-v18.17.0-linux-x64.tar.xz", "node-v18.17.0-linux-x64", "f36facda28c4d5ce76b3a1b4344e688d29d9254943a47f2f1909b1a10acb1959"), - "18.17.0-windows_amd64": ("node-v18.17.0-win-x64.zip", "node-v18.17.0-win-x64", "06e30b4e70b18d794651ef132c39080e5eaaa1187f938721d57edae2824f4e96"), + "18.17.1-darwin_amd64": ("node-v18.17.1-darwin-x64.tar.gz", "node-v18.17.1-darwin-x64", "b3e083d2715f07ec3f00438401fb58faa1e0bdf3c7bde9f38b75ed17809d92fa"), + "18.17.1-darwin_arm64": ("node-v18.17.1-darwin-arm64.tar.gz", "node-v18.17.1-darwin-arm64", "18ca716ea57522b90473777cb9f878467f77fdf826d37beb15a0889fdd74533e"), + "18.17.1-linux_arm64": ("node-v18.17.1-linux-arm64.tar.xz", "node-v18.17.1-linux-arm64", "3f933716a468524acb68c2514d819b532131eb50399ee946954d4a511303e1bb"), + "18.17.1-linux_amd64": ("node-v18.17.1-linux-x64.tar.xz", "node-v18.17.1-linux-x64", "07e76408ddb0300a6f46fcc9abc61f841acde49b45020ec4e86bb9b25df4dced"), + "18.17.1-windows_amd64": ("node-v18.17.1-win-x64.zip", "node-v18.17.1-win-x64", "afc83f5cf6e8b45a4d3fb842904f604dcd271fefada31ad6654f8302f8da28c9"), }, - node_version = "18.17.0", + node_version = "18.17.1", node_urls = [ "https://nodejs.org/dist/v{version}/{filename}", ], diff --git a/config/serverless.yml b/config/serverless.yml index f874fc0971f8b..96cdcd7f52d15 100644 --- a/config/serverless.yml +++ b/config/serverless.yml @@ -32,15 +32,18 @@ xpack.remote_clusters.enabled: false xpack.snapshot_restore.enabled: false xpack.license_management.enabled: false -# Disable index management actions from the UI +# Management team UI configurations +# Disable index actions from the Index Management UI xpack.index_management.enableIndexActions: false +# Disable legacy index templates from Index Management UI +xpack.index_management.enableLegacyTemplates: false # Keep deeplinks visible so that they are shown in the sidenav dev_tools.deeplinks.navLinkStatus: visible management.deeplinks.navLinkStatus: visible # Other disabled plugins -#xpack.canvas.enabled: false #only disabable in dev-mode +xpack.canvas.enabled: false xpack.cloud_integrations.data_migration.enabled: false data.search.sessions.enabled: false advanced_settings.enabled: false diff --git a/docs/developer/advanced/upgrading-nodejs.asciidoc b/docs/developer/advanced/upgrading-nodejs.asciidoc index 3f27d5a62147d..9587dfbfd14a0 100644 --- a/docs/developer/advanced/upgrading-nodejs.asciidoc +++ b/docs/developer/advanced/upgrading-nodejs.asciidoc @@ -17,7 +17,7 @@ These files must be updated when upgrading Node.js: - {kib-repo}blob/{branch}/WORKSPACE.bazel[`WORKSPACE.bazel`] - The version is specified in the `node_version` property. Besides this property, the list of files under `node_repositories` must be updated along with their respective SHA256 hashes. These can be found on the https://nodejs.org[nodejs.org] website. - Example for Node.js v18.17.0: https://nodejs.org/dist/v18.17.0/SHASUMS256.txt.asc + Example for Node.js v18.17.1: https://nodejs.org/dist/v18.17.1/SHASUMS256.txt.asc See PR {kib-repo}pull/128123[#128123] for an example of how the Node.js version has been upgraded previously. diff --git a/docs/user/alerting/create-and-manage-rules.asciidoc b/docs/user/alerting/create-and-manage-rules.asciidoc index ed21a2bc8b228..31c43346ef308 100644 --- a/docs/user/alerting/create-and-manage-rules.asciidoc +++ b/docs/user/alerting/create-and-manage-rules.asciidoc @@ -71,22 +71,28 @@ conditions are met and when they are no longer met. Each action uses a connector, which provides connection information for a {kib} service or third party integration, depending on where you want to send the notifications. If no connectors exist, click **Add connector** to create one. -After you select a connector, set the action frequency. If the rule type supports alert summaries, you can choose to create a summary of alerts on each check interval or on a custom interval. For example, if you create a metrics threshold rule, you can send email notifications that summarize the new, ongoing, and recovered alerts each day: +After you select a connector, set the action frequency. If the rule type supports alert summaries, you can choose to create a summary of alerts on each check interval or on a custom interval. For example, if you create a metrics threshold rule, you can send email notifications that summarize the new, ongoing, and recovered alerts each hour: [role="screenshot"] -image::images/rule-flyout-action-summary.png[UI for defining rule conditions on a metric threshold rule,500] +image::images/action-alert-summary.png[UI for defining rule conditions on a metric threshold rule,500] +// NOTE: This is an autogenerated screenshot. Do not edit it directly. -TIP: If you choose a custom action interval, it cannot be shorter than the rule's check interval. +[NOTE] +==== +* The rules that support alert summaries, such as this metric threshold rule, enable you to further refine when actions run by adding time frame and query filters. +* If you choose a custom action interval, it cannot be shorter than the rule's check interval. +==== -Alternatively, you can set the action frequency such that the action runs for each alert. If the rule type does not support alert summaries, this is your only available option. You must choose when the action runs (for example, at each check interval, only when the alert status changes, or at a custom action interval). You must also choose an action group, which affects whether the action runs (for example, the action runs when the issue is detected or when it is recovered). Each rule type has a specific set of valid action groups. +Alternatively, you can set the action frequency such that the action runs for each alert. +If the rule type does not support alert summaries, this is your only available option. +You must choose when the action runs (for example, at each check interval, only when the alert status changes, or at a custom action interval). +You must also choose an action group, which affects whether the action runs. Each rule type has a specific set of valid action groups. +For example, you can set *Run when* to `Alert`, `Warning`, `No data`, or `Recovered` for the metric threshold rule: [role="screenshot"] image::images/rule-flyout-action-details.png[UI for defining an email action,500] // NOTE: This is an autogenerated screenshot. Do not edit it directly. -If you create rules in the {security-app}, you can further refine when actions run by adding time frame and query filters. -For more details, refer to {security-guide}/rules-ui-create.html[Create a detection rule]. - Each connector enables different action properties. For example, an email connector enables you to set the recipients, the subject, and a message body in markdown format. For more information about connectors, refer to <>. [[alerting-concepts-suppressing-duplicate-notifications]] diff --git a/docs/user/alerting/images/action-alert-summary.png b/docs/user/alerting/images/action-alert-summary.png new file mode 100644 index 0000000000000..038e346a72725 Binary files /dev/null and b/docs/user/alerting/images/action-alert-summary.png differ diff --git a/docs/user/alerting/images/rule-flyout-action-summary.png b/docs/user/alerting/images/rule-flyout-action-summary.png deleted file mode 100644 index f6fe3ba1ee9f5..0000000000000 Binary files a/docs/user/alerting/images/rule-flyout-action-summary.png and /dev/null differ diff --git a/package.json b/package.json index 16f0ec705f349..61f6c2093f0b6 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "url": "https://github.com/elastic/kibana.git" }, "engines": { - "node": "18.17.0", + "node": "18.17.1", "yarn": "^1.22.19" }, "resolutions": { @@ -495,6 +495,7 @@ "@kbn/logstash-plugin": "link:x-pack/plugins/logstash", "@kbn/management-cards-navigation": "link:packages/kbn-management/cards_navigation", "@kbn/management-plugin": "link:src/plugins/management", + "@kbn/management-settings-section-registry": "link:packages/kbn-management/settings/section_registry", "@kbn/management-test-plugin": "link:test/plugin_functional/plugins/management_test_plugin", "@kbn/mapbox-gl": "link:packages/kbn-mapbox-gl", "@kbn/maps-custom-raster-source-plugin": "link:x-pack/examples/third_party_maps_source_example", diff --git a/packages/kbn-apm-synthtrace/src/cli/utils/get_service_urls.ts b/packages/kbn-apm-synthtrace/src/cli/utils/get_service_urls.ts index 67a319b8dd498..64abc36c05602 100644 --- a/packages/kbn-apm-synthtrace/src/cli/utils/get_service_urls.ts +++ b/packages/kbn-apm-synthtrace/src/cli/utils/get_service_urls.ts @@ -80,8 +80,8 @@ async function getKibanaUrl({ target, logger }: { target: string; logger: Logger export async function getServiceUrls({ logger, target, kibana }: RunOptions & { logger: Logger }) { if (!target) { // assume things are running locally - kibana = kibana || 'http://localhost:5601'; - target = 'http://localhost:9200'; + kibana = kibana || 'http://127.0.0.1:5601'; + target = 'http://127.0.0.1:9200'; } if (!target) { diff --git a/packages/kbn-management/settings/section_registry/README.mdx b/packages/kbn-management/settings/section_registry/README.mdx new file mode 100644 index 0000000000000..87a957e611fc5 --- /dev/null +++ b/packages/kbn-management/settings/section_registry/README.mdx @@ -0,0 +1,56 @@ +--- +id: kbn-management/settings/SectionRegistry +slug: /kbn-management/settings/section-registry/ +title: Section Registry +description: A registry which allows a consumer to add sections to Advanced Settings. +tags: ['management', 'settings'] +date: 2023-08-04 +--- + +This registry is fairly straightforward: it allows a consumer to add a section to the Advanced Settings page. This registry would be consumed by a plugin and exposed on the `start` and `setup` contracts: + +```ts + +const registry = new SectionRegistry(); + +export class PluginBar + implements Plugin +{ + public setup( + _core: CoreSetup, + _setupDeps: PluginFooSetupDeps + ) { + return { + sectionRegistry: sectionRegistry.setup, + }; + } + + public start( + _core: CoreStart, + _startDeps: PluginFooStartDeps + ) { + return { + sectionRegistry: sectionRegistry.start, + }; + } +} + +export class PluginFoo + implements Plugin +{ + public setup( + core: CoreSetup, + { pluginBar: { sectionRegistry } }: PluginFooSetupDeps + ) { + const Component = (props: RegistryComponentProps) => ; + + const queryMatch = (query: string) => { + const searchTerm = query.toLowerCase(); + return SEARCH_TERMS.some((term) => term.indexOf(searchTerm) >= 0); + }; + + sectionRegistry.setup.addGlobalSection(Component, queryMatch); + } +} + +``` diff --git a/src/plugins/advanced_settings/public/component_registry/index.ts b/packages/kbn-management/settings/section_registry/index.ts similarity index 65% rename from src/plugins/advanced_settings/public/component_registry/index.ts rename to packages/kbn-management/settings/section_registry/index.ts index a147d61a72cf0..3cc7ffc48cd15 100644 --- a/src/plugins/advanced_settings/public/component_registry/index.ts +++ b/packages/kbn-management/settings/section_registry/index.ts @@ -6,4 +6,10 @@ * Side Public License, v 1. */ -export { ComponentRegistry } from './component_registry'; +export { SectionRegistry } from './section_registry'; +export type { + SectionRegistrySetup, + SectionRegistryStart, + RegistryComponentProps, + RegistryEntry, +} from './section_registry'; diff --git a/packages/kbn-management/settings/section_registry/jest.config.js b/packages/kbn-management/settings/section_registry/jest.config.js new file mode 100644 index 0000000000000..f183446f77bc6 --- /dev/null +++ b/packages/kbn-management/settings/section_registry/jest.config.js @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../../..', + roots: ['/packages/kbn-management/settings/section_registry'], + coverageDirectory: + '/target/kibana-coverage/jest/packages/kbn-management/settings/section_registry', + coverageReporters: ['text', 'html'], + collectCoverageFrom: [ + '/packages/kbn-management/settings/section_registry/**/*.{ts,tsx}', + ], +}; diff --git a/packages/kbn-management/settings/section_registry/kibana.jsonc b/packages/kbn-management/settings/section_registry/kibana.jsonc new file mode 100644 index 0000000000000..86e242165a8e8 --- /dev/null +++ b/packages/kbn-management/settings/section_registry/kibana.jsonc @@ -0,0 +1,5 @@ +{ + "type": "shared-common", + "id": "@kbn/management-settings-section-registry", + "owner": "@elastic/appex-sharedux @elastic/platform-deployment-management" +} \ No newline at end of file diff --git a/packages/kbn-management/settings/section_registry/package.json b/packages/kbn-management/settings/section_registry/package.json new file mode 100644 index 0000000000000..6138c9ecc5b8c --- /dev/null +++ b/packages/kbn-management/settings/section_registry/package.json @@ -0,0 +1,6 @@ +{ + "name": "@kbn/management-settings-section-registry", + "private": true, + "version": "1.0.0", + "license": "SSPL-1.0 OR Elastic License 2.0" +} \ No newline at end of file diff --git a/packages/kbn-management/settings/section_registry/section_registry.test.tsx b/packages/kbn-management/settings/section_registry/section_registry.test.tsx new file mode 100644 index 0000000000000..ad25d3158982d --- /dev/null +++ b/packages/kbn-management/settings/section_registry/section_registry.test.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { SectionRegistry } from './section_registry'; + +describe('SectionRegistry', () => { + let registry = new SectionRegistry(); + + beforeEach(() => { + registry = new SectionRegistry(); + }); + + describe('register', () => { + it('should allow a global component to be registered', () => { + const Component = () =>
; + const queryMatch = () => true; + registry.setup.addGlobalSection(Component, queryMatch); + + const entries = registry.start.getGlobalSections(); + expect(entries).toHaveLength(1); + expect(entries[0].Component).toBe(Component); + expect(entries[0].queryMatch).toBe(queryMatch); + expect(registry.start.getSpacesSections()).toHaveLength(0); + }); + + it('should allow a spaces component to be registered', () => { + const Component = () =>
; + const queryMatch = () => true; + registry.setup.addSpaceSection(Component, queryMatch); + + const entries = registry.start.getSpacesSections(); + expect(entries).toHaveLength(1); + expect(entries[0].Component).toBe(Component); + expect(entries[0].queryMatch).toBe(queryMatch); + expect(registry.start.getGlobalSections()).toHaveLength(0); + }); + }); +}); diff --git a/packages/kbn-management/settings/section_registry/section_registry.ts b/packages/kbn-management/settings/section_registry/section_registry.ts new file mode 100644 index 0000000000000..0507bd8e23aac --- /dev/null +++ b/packages/kbn-management/settings/section_registry/section_registry.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { ComponentType } from 'react'; +import { ToastsStart } from '@kbn/core-notifications-browser'; +import { UiSettingsScope } from '@kbn/core-ui-settings-common'; + +/** + * Props provided to a `RegistryComponent`. + */ +export interface RegistryComponentProps { + toasts: ToastsStart; + enableSaving: Record; +} + +/** + * A registry entry for a section. + */ +export interface RegistryEntry { + Component: RegistryComponent; + queryMatch: QueryMatchFn; +} + +type RegistryComponent = ComponentType; +type PageType = 'space' | 'global'; +type QueryMatchFn = (term: string) => boolean; +type Registry = Record; + +/** + * A registry of sections to add to pages within Advanced Settings. + */ +export class SectionRegistry { + private registry: Registry = { + space: [], + global: [], + }; + + setup = { + /** + * Registers a section within the "Space" page. + * + * @param Component - A React component to render. + * @param queryMatch - A function that, given a search term, returns true if the section should be rendered. + */ + addSpaceSection: (Component: RegistryComponent, queryMatch: QueryMatchFn) => { + this.registry.space.push({ Component, queryMatch }); + }, + + /** + * Registers a section within the "Global" page. + * + * @param Component - A React component to render. + * @param queryMatch - A function that, given a search term, returns true if the section should be rendered. + */ + addGlobalSection: (Component: RegistryComponent, queryMatch: QueryMatchFn) => { + this.registry.global.push({ Component, queryMatch }); + }, + }; + + start = { + /** + * Retrieve components registered for the "Space" page. + */ + getGlobalSections: (): RegistryEntry[] => { + return this.registry.global; + }, + + /** + * Retrieve components registered for the "Global" page. + */ + getSpacesSections: (): RegistryEntry[] => { + return this.registry.space; + }, + }; +} + +/** + * The `setup` contract provided by a `SectionRegistry`. + */ +export type SectionRegistrySetup = SectionRegistry['setup']; + +/** + * The `start` contract provided by a `SectionRegistry`. + */ +export type SectionRegistryStart = SectionRegistry['start']; diff --git a/packages/kbn-management/settings/section_registry/tsconfig.json b/packages/kbn-management/settings/section_registry/tsconfig.json new file mode 100644 index 0000000000000..2cf1893be5d55 --- /dev/null +++ b/packages/kbn-management/settings/section_registry/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "target/types", + "types": [ + "jest", + "node", + "react" + ] + }, + "include": [ + "**/*.ts", + "**/*.tsx", + ], + "exclude": [ + "target/**/*" + ], + "kbn_references": [ + "@kbn/core-notifications-browser", + "@kbn/core-ui-settings-common", + ] +} diff --git a/src/plugins/advanced_settings/kibana.jsonc b/src/plugins/advanced_settings/kibana.jsonc index 99362de44c891..c0106e24c50f4 100644 --- a/src/plugins/advanced_settings/kibana.jsonc +++ b/src/plugins/advanced_settings/kibana.jsonc @@ -1,7 +1,7 @@ { "type": "plugin", "id": "@kbn/advanced-settings-plugin", - "owner": "@elastic/appex-sharedux", + "owner": "@elastic/appex-sharedux @elastic/platform-deployment-management", "plugin": { "id": "advancedSettings", "server": true, @@ -18,4 +18,4 @@ "kibanaUtils" ] } -} +} \ No newline at end of file diff --git a/src/plugins/advanced_settings/public/component_registry/__snapshots__/component_registry.test.tsx.snap b/src/plugins/advanced_settings/public/component_registry/__snapshots__/component_registry.test.tsx.snap deleted file mode 100644 index 1d6cc882cb344..0000000000000 --- a/src/plugins/advanced_settings/public/component_registry/__snapshots__/component_registry.test.tsx.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ComponentRegistry register should disallow registering a component with a duplicate id 1`] = `"Component with id advanced_settings_page_title is already registered."`; diff --git a/src/plugins/advanced_settings/public/component_registry/component_registry.test.tsx b/src/plugins/advanced_settings/public/component_registry/component_registry.test.tsx deleted file mode 100644 index a04caaa40da7e..0000000000000 --- a/src/plugins/advanced_settings/public/component_registry/component_registry.test.tsx +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { ComponentRegistry } from './component_registry'; - -describe('ComponentRegistry', () => { - describe('register', () => { - it('should allow a component to be registered', () => { - const component = () =>
; - new ComponentRegistry().setup.register( - ComponentRegistry.componentType.PAGE_TITLE_COMPONENT, - component - ); - }); - - it('should disallow registering a component with a duplicate id', () => { - const registry = new ComponentRegistry(); - const component = () =>
; - registry.setup.register(ComponentRegistry.componentType.PAGE_TITLE_COMPONENT, component); - expect(() => - registry.setup.register(ComponentRegistry.componentType.PAGE_TITLE_COMPONENT, () => ( - - )) - ).toThrowErrorMatchingSnapshot(); - }); - - it('should allow a component to be overriden', () => { - const registry = new ComponentRegistry(); - const component = () =>
; - registry.setup.register(ComponentRegistry.componentType.PAGE_TITLE_COMPONENT, component); - - const anotherComponent = () => ; - registry.setup.register( - ComponentRegistry.componentType.PAGE_TITLE_COMPONENT, - anotherComponent, - true - ); - - expect(registry.start.get(ComponentRegistry.componentType.PAGE_TITLE_COMPONENT)).toBe( - anotherComponent - ); - }); - }); - - describe('get', () => { - it('should allow a component to be retrieved', () => { - const registry = new ComponentRegistry(); - const component = () =>
; - registry.setup.register(ComponentRegistry.componentType.PAGE_TITLE_COMPONENT, component); - expect(registry.start.get(ComponentRegistry.componentType.PAGE_TITLE_COMPONENT)).toBe( - component - ); - }); - }); - - it('should set a displayName for the component if one does not exist', () => { - const component: React.ComponentType = () =>
; - const registry = new ComponentRegistry(); - registry.setup.register(ComponentRegistry.componentType.PAGE_TITLE_COMPONENT, component); - - expect(component.displayName).toEqual(ComponentRegistry.componentType.PAGE_TITLE_COMPONENT); - }); - - it('should not set a displayName for the component if one already exists', () => { - const component: React.ComponentType = () =>
; - component.displayName = ''; - const registry = new ComponentRegistry(); - - registry.setup.register(ComponentRegistry.componentType.PAGE_TITLE_COMPONENT, component); - - expect(component.displayName).toEqual(''); - }); -}); diff --git a/src/plugins/advanced_settings/public/component_registry/component_registry.ts b/src/plugins/advanced_settings/public/component_registry/component_registry.ts deleted file mode 100644 index 93b0ab67b50cf..0000000000000 --- a/src/plugins/advanced_settings/public/component_registry/component_registry.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { ComponentType } from 'react'; -import { PageTitle } from './page_title'; -import { PageSubtitle } from './page_subtitle'; -import { PageFooter } from './page_footer'; - -type Id = - | 'advanced_settings_page_title' - | 'advanced_settings_page_subtitle' - | 'advanced_settings_page_footer'; - -const componentType: { [key: string]: Id } = { - PAGE_TITLE_COMPONENT: 'advanced_settings_page_title' as Id, - PAGE_SUBTITLE_COMPONENT: 'advanced_settings_page_subtitle' as Id, - PAGE_FOOTER_COMPONENT: 'advanced_settings_page_footer' as Id, -}; - -type RegistryComponent = ComponentType | undefined>; - -export class ComponentRegistry { - static readonly componentType = componentType; - static readonly defaultRegistry: Record = { - advanced_settings_page_title: PageTitle, - advanced_settings_page_subtitle: PageSubtitle, - advanced_settings_page_footer: PageFooter, - }; - - registry: { [key in Id]?: RegistryComponent } = {}; - - setup = { - componentType: ComponentRegistry.componentType, - /** - * Attempts to register the provided component, with the ability to optionally allow - * the component to override an existing one. - * - * If the intent is to override, then `allowOverride` must be set to true, otherwise an exception is thrown. - * - * @param id the id of the component to register - * @param component the component - * @param allowOverride (default: false) - optional flag to allow this component to override a previously registered component - */ - register: (id: Id, component: RegistryComponent, allowOverride = false) => { - if (!allowOverride && id in this.registry) { - throw new Error(`Component with id ${id} is already registered.`); - } - - // Setting a display name if one does not already exist. - // This enhances the snapshots, as well as the debugging experience. - if (!component.displayName) { - component.displayName = id; - } - - this.registry[id] = component; - }, - }; - - start = { - componentType: ComponentRegistry.componentType, - /** - * Retrieve a registered component by its ID. - * If the component does not exist, then an exception is thrown. - * - * @param id the ID of the component to retrieve - */ - get: (id: Id): RegistryComponent => { - return this.registry[id] || ComponentRegistry.defaultRegistry[id]; - }, - }; -} diff --git a/src/plugins/advanced_settings/public/component_registry/page_footer/__snapshots__/page_footer.test.tsx.snap b/src/plugins/advanced_settings/public/component_registry/page_footer/__snapshots__/page_footer.test.tsx.snap deleted file mode 100644 index eea1003c8eb95..0000000000000 --- a/src/plugins/advanced_settings/public/component_registry/page_footer/__snapshots__/page_footer.test.tsx.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PageFooter should render normally 1`] = `""`; diff --git a/src/plugins/advanced_settings/public/component_registry/page_footer/page_footer.test.tsx b/src/plugins/advanced_settings/public/component_registry/page_footer/page_footer.test.tsx deleted file mode 100644 index b65c5c5020533..0000000000000 --- a/src/plugins/advanced_settings/public/component_registry/page_footer/page_footer.test.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { shallowWithI18nProvider } from '@kbn/test-jest-helpers'; - -import { PageFooter } from './page_footer'; - -describe('PageFooter', () => { - it('should render normally', () => { - expect(shallowWithI18nProvider()).toMatchSnapshot(); - }); -}); diff --git a/src/plugins/advanced_settings/public/component_registry/page_footer/page_footer.ts b/src/plugins/advanced_settings/public/component_registry/page_footer/page_footer.ts deleted file mode 100644 index f8de35b601a50..0000000000000 --- a/src/plugins/advanced_settings/public/component_registry/page_footer/page_footer.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export const PageFooter = () => null; diff --git a/src/plugins/advanced_settings/public/component_registry/page_subtitle/__snapshots__/page_subtitle.test.tsx.snap b/src/plugins/advanced_settings/public/component_registry/page_subtitle/__snapshots__/page_subtitle.test.tsx.snap deleted file mode 100644 index 24ec895459038..0000000000000 --- a/src/plugins/advanced_settings/public/component_registry/page_subtitle/__snapshots__/page_subtitle.test.tsx.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PageSubtitle should render normally 1`] = `""`; diff --git a/src/plugins/advanced_settings/public/component_registry/page_subtitle/index.ts b/src/plugins/advanced_settings/public/component_registry/page_subtitle/index.ts deleted file mode 100644 index bec1741483081..0000000000000 --- a/src/plugins/advanced_settings/public/component_registry/page_subtitle/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export { PageSubtitle } from './page_subtitle'; diff --git a/src/plugins/advanced_settings/public/component_registry/page_subtitle/page_subtitle.test.tsx b/src/plugins/advanced_settings/public/component_registry/page_subtitle/page_subtitle.test.tsx deleted file mode 100644 index 792721490256f..0000000000000 --- a/src/plugins/advanced_settings/public/component_registry/page_subtitle/page_subtitle.test.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { shallowWithI18nProvider } from '@kbn/test-jest-helpers'; - -import { PageSubtitle } from './page_subtitle'; - -describe('PageSubtitle', () => { - it('should render normally', () => { - expect(shallowWithI18nProvider()).toMatchSnapshot(); - }); -}); diff --git a/src/plugins/advanced_settings/public/component_registry/page_subtitle/page_subtitle.ts b/src/plugins/advanced_settings/public/component_registry/page_subtitle/page_subtitle.ts deleted file mode 100644 index cc35fbcc4fe22..0000000000000 --- a/src/plugins/advanced_settings/public/component_registry/page_subtitle/page_subtitle.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export const PageSubtitle = () => null; diff --git a/src/plugins/advanced_settings/public/component_registry/page_title/__snapshots__/page_title.test.tsx.snap b/src/plugins/advanced_settings/public/component_registry/page_title/__snapshots__/page_title.test.tsx.snap deleted file mode 100644 index 10b799a986b84..0000000000000 --- a/src/plugins/advanced_settings/public/component_registry/page_title/__snapshots__/page_title.test.tsx.snap +++ /dev/null @@ -1,15 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`PageTitle should render normally 1`] = ` - -

- -

-
-`; diff --git a/src/plugins/advanced_settings/public/component_registry/page_title/index.ts b/src/plugins/advanced_settings/public/component_registry/page_title/index.ts deleted file mode 100644 index 94585eabbd11b..0000000000000 --- a/src/plugins/advanced_settings/public/component_registry/page_title/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export { PageTitle } from './page_title'; diff --git a/src/plugins/advanced_settings/public/component_registry/page_title/page_title.test.tsx b/src/plugins/advanced_settings/public/component_registry/page_title/page_title.test.tsx deleted file mode 100644 index 05d44f3aee84d..0000000000000 --- a/src/plugins/advanced_settings/public/component_registry/page_title/page_title.test.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { shallowWithI18nProvider } from '@kbn/test-jest-helpers'; - -import { PageTitle } from './page_title'; - -describe('PageTitle', () => { - it('should render normally', () => { - expect(shallowWithI18nProvider()).toMatchSnapshot(); - }); -}); diff --git a/src/plugins/advanced_settings/public/component_registry/page_title/page_title.tsx b/src/plugins/advanced_settings/public/component_registry/page_title/page_title.tsx deleted file mode 100644 index 018644b3a9f9a..0000000000000 --- a/src/plugins/advanced_settings/public/component_registry/page_title/page_title.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import React from 'react'; -import { EuiText } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; - -export const PageTitle = () => { - return ( - -

- -

-
- ); -}; diff --git a/src/plugins/advanced_settings/public/index.ts b/src/plugins/advanced_settings/public/index.ts index 6d6bf6f055f41..e07c1aa1d72d1 100644 --- a/src/plugins/advanced_settings/public/index.ts +++ b/src/plugins/advanced_settings/public/index.ts @@ -10,7 +10,6 @@ import React from 'react'; import { PluginInitializerContext } from '@kbn/core/public'; import { AdvancedSettingsPlugin } from './plugin'; export type { AdvancedSettingsSetup, AdvancedSettingsStart } from './types'; -export { ComponentRegistry } from './component_registry'; /** * Exports the field component as a React.lazy component. We're explicitly naming it lazy here diff --git a/src/plugins/advanced_settings/public/management_app/i18n_texts.ts b/src/plugins/advanced_settings/public/management_app/i18n_texts.ts index d1edd5eed09e0..5267e4baa8029 100644 --- a/src/plugins/advanced_settings/public/management_app/i18n_texts.ts +++ b/src/plugins/advanced_settings/public/management_app/i18n_texts.ts @@ -13,7 +13,7 @@ export const i18nTexts = { defaultMessage: 'Space Settings', }), defaultSpaceCalloutTitle: i18n.translate('advancedSettings.defaultSpaceCalloutTitle', { - defaultMessage: 'Changes will affect the `default` space', + defaultMessage: 'Changes will affect the current space.', }), defaultSpaceCalloutSubtitle: i18n.translate('advancedSettings.defaultSpaceCalloutSubtitle', { defaultMessage: diff --git a/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx b/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx index bfa1389904ee7..d41f1f1cacfe4 100644 --- a/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx +++ b/src/plugins/advanced_settings/public/management_app/mount_management_section.tsx @@ -12,18 +12,17 @@ import { Redirect, RouteChildrenProps } from 'react-router-dom'; import { Router, Routes, Route } from '@kbn/shared-ux-router'; import { i18n } from '@kbn/i18n'; -import { I18nProvider } from '@kbn/i18n-react'; import { LocationDescriptor } from 'history'; -import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public'; import { url } from '@kbn/kibana-utils-plugin/public'; import { ManagementAppMountParams } from '@kbn/management-plugin/public'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import { StartServicesAccessor } from '@kbn/core/public'; +import type { SectionRegistryStart } from '@kbn/management-settings-section-registry'; +import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render'; import { QUERY } from './advanced_settings'; import { Settings } from './settings'; -import { ComponentRegistry } from '../types'; import './index.scss'; @@ -56,11 +55,12 @@ const redirectUrl = ({ match, location }: RedirectUrlProps): LocationDescriptor export async function mountManagementSection( getStartServices: StartServicesAccessor, params: ManagementAppMountParams, - componentRegistry: ComponentRegistry['start'], + sectionRegistry: SectionRegistryStart, usageCollection?: UsageCollectionSetup ) { params.setBreadcrumbs(crumb); - const [{ settings, notifications, docLinks, application, chrome }] = await getStartServices(); + const [{ settings, notifications, docLinks, application, chrome, i18n: i18nStart, theme }] = + await getStartServices(); const { advancedSettings, globalSettings } = application.capabilities; const canSaveAdvancedSettings = advancedSettings.save as boolean; @@ -74,34 +74,32 @@ export async function mountManagementSection( chrome.docTitle.change(title); ReactDOM.render( - - - - - {/* TODO: remove route param (`query`) in 7.13 */} - - {(props: RedirectUrlProps) => } - - - - - - - - , + + + + {/* TODO: remove route param (`query`) in 7.13 */} + + {(props: RedirectUrlProps) => } + + + + + + + , params.element ); return () => { diff --git a/src/plugins/advanced_settings/public/management_app/settings.test.tsx b/src/plugins/advanced_settings/public/management_app/settings.test.tsx index 59c4cba601535..f51eec4e97bb6 100644 --- a/src/plugins/advanced_settings/public/management_app/settings.test.tsx +++ b/src/plugins/advanced_settings/public/management_app/settings.test.tsx @@ -21,7 +21,7 @@ import { docLinksServiceMock, themeServiceMock, } from '@kbn/core/public/mocks'; -import { ComponentRegistry } from '../component_registry'; +import { SectionRegistry } from '@kbn/management-settings-section-registry'; import { Search } from './components/search'; import { EuiTab } from '@elastic/eui'; @@ -257,7 +257,7 @@ describe('Settings', () => { toasts={notificationServiceMock.createStartContract().toasts} docLinks={docLinksServiceMock.createStartContract().links} settingsService={mockConfig().core.settings} - componentRegistry={new ComponentRegistry().start} + sectionRegistry={new SectionRegistry().start} theme={themeServiceMock.createStartContract().theme$} /> ); @@ -286,7 +286,7 @@ describe('Settings', () => { toasts={notificationServiceMock.createStartContract().toasts} docLinks={docLinksServiceMock.createStartContract().links} settingsService={mockConfig().core.settings} - componentRegistry={new ComponentRegistry().start} + sectionRegistry={new SectionRegistry().start} theme={themeServiceMock.createStartContract().theme$} /> ); @@ -312,7 +312,7 @@ describe('Settings', () => { toasts={notificationServiceMock.createStartContract().toasts} docLinks={docLinksServiceMock.createStartContract().links} settingsService={mockConfig().core.settings} - componentRegistry={new ComponentRegistry().start} + sectionRegistry={new SectionRegistry().start} theme={themeServiceMock.createStartContract().theme$} /> ); @@ -341,7 +341,7 @@ describe('Settings', () => { toasts={toasts} docLinks={docLinksServiceMock.createStartContract().links} settingsService={mockConfig().core.settings} - componentRegistry={new ComponentRegistry().start} + sectionRegistry={new SectionRegistry().start} theme={themeServiceMock.createStartContract().theme$} /> ); @@ -363,7 +363,7 @@ describe('Settings', () => { toasts={toasts} docLinks={docLinksServiceMock.createStartContract().links} settingsService={mockConfig().core.settings} - componentRegistry={new ComponentRegistry().start} + sectionRegistry={new SectionRegistry().start} theme={themeServiceMock.createStartContract().theme$} /> ); diff --git a/src/plugins/advanced_settings/public/management_app/settings.tsx b/src/plugins/advanced_settings/public/management_app/settings.tsx index 0a091aa55a481..ae5837a97cbec 100644 --- a/src/plugins/advanced_settings/public/management_app/settings.tsx +++ b/src/plugins/advanced_settings/public/management_app/settings.tsx @@ -26,19 +26,23 @@ import { UiCounterMetricType } from '@kbn/analytics'; import { url } from '@kbn/kibana-utils-plugin/common'; import { parse } from 'query-string'; import { UiSettingsScope } from '@kbn/core-ui-settings-common'; +import type { SectionRegistryStart } from '@kbn/management-settings-section-registry'; +import type { RegistryEntry } from '@kbn/management-settings-section-registry'; import { mapConfig, mapSettings, initCategoryCounts, initCategories } from './settings_helper'; import { parseErrorMsg } from './components/search/search'; import { AdvancedSettings, QUERY } from './advanced_settings'; -import { ComponentRegistry } from '..'; import { Search } from './components/search'; import { FieldSetting } from './types'; import { i18nTexts } from './i18n_texts'; import { getAriaName } from './lib'; interface AdvancedSettingsState { - footerQueryMatched: boolean; query: Query; filteredSettings: Record>; + filteredSections: { + global: RegistryEntry[]; + space: RegistryEntry[]; + }; } export type GroupedSettings = Record; @@ -51,7 +55,7 @@ interface Props { docLinks: DocLinksStart['links']; toasts: ToastsStart; theme: ThemeServiceStart['theme$']; - componentRegistry: ComponentRegistry['start']; + sectionRegistry: SectionRegistryStart; trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; } @@ -59,8 +63,7 @@ const SPACE_SETTINGS_ID = 'space-settings'; const GLOBAL_SETTINGS_ID = 'global-settings'; export const Settings = (props: Props) => { - const { componentRegistry, history, settingsService, enableSaving, enableShowing, ...rest } = - props; + const { sectionRegistry, history, settingsService, enableSaving, enableShowing, ...rest } = props; const uiSettings = settingsService.client; const globalUiSettings = settingsService.globalClient; @@ -89,7 +92,10 @@ export const Settings = (props: Props) => { global: {}, namespace: {}, }, - footerQueryMatched: false, + filteredSections: { + global: sectionRegistry.getGlobalSections(), + space: sectionRegistry.getSpacesSections(), + }, query: Query.parse(''), }); @@ -206,7 +212,9 @@ export const Settings = (props: Props) => { categories={categories[scope]} visibleSettings={queryState.filteredSettings[scope]} clearQuery={() => setUrlQuery('')} - noResults={!queryState.footerQueryMatched} + noResults={ + queryState.filteredSections.global.length + queryState.filteredSections.space.length === 0 + } queryText={queryState.query.text} callOutTitle={callOutTitle(scope)} callOutSubtitle={callOutSubtitle(scope)} @@ -225,7 +233,8 @@ export const Settings = (props: Props) => { append: queryState.query.text !== '' ? ( - {Object.keys(queryState.filteredSettings.namespace).length} + {Object.keys(queryState.filteredSettings.namespace).length + + queryState.filteredSections.space.length} ) : null, content: renderAdvancedSettings('namespace'), @@ -239,7 +248,7 @@ export const Settings = (props: Props) => { queryState.query.text !== '' ? ( {Object.keys(queryState.filteredSettings.global).length + - Number(queryState.footerQueryMatched)} + queryState.filteredSections.global.length} ) : null, content: renderAdvancedSettings('global'), @@ -297,7 +306,14 @@ export const Settings = (props: Props) => { return { query, filteredSettings, - footerQueryMatched: initialQuery ? false : queryState.footerQueryMatched, + filteredSections: { + global: sectionRegistry + .getGlobalSections() + .filter(({ queryMatch }) => queryMatch(query.text)), + space: sectionRegistry + .getSpacesSections() + .filter(({ queryMatch }) => queryMatch(query.text)), + }, }; }; @@ -308,19 +324,25 @@ export const Settings = (props: Props) => { [setUrlQuery] ); - const onFooterQueryMatchChange = useCallback( - (matched: boolean) => { - setQueryState({ ...queryState, footerQueryMatched: matched }); - }, - [queryState] - ); - const PageTitle = (

{i18nTexts.advancedSettingsTitle}

); - const PageFooter = componentRegistry.get(componentRegistry.componentType.PAGE_FOOTER_COMPONENT); + + const mapSections = (entries: RegistryEntry[]) => + entries.map(({ Component, queryMatch }, index) => { + if (queryMatch(queryState.query.text)) { + return ( + + ); + } + return null; + }); return (
@@ -337,14 +359,11 @@ export const Settings = (props: Props) => { {renderTabs()} {selectedTabContent} - {selectedTabId === GLOBAL_SETTINGS_ID ? ( - - ) : null} + {selectedTabId === SPACE_SETTINGS_ID ? ( + <>{mapSections(queryState.filteredSections.space)} + ) : ( + <>{mapSections(queryState.filteredSections.global)} + )}
); }; diff --git a/src/plugins/advanced_settings/public/mocks.ts b/src/plugins/advanced_settings/public/mocks.ts index 5c65e66a7c82e..00e50b6672e07 100644 --- a/src/plugins/advanced_settings/public/mocks.ts +++ b/src/plugins/advanced_settings/public/mocks.ts @@ -6,17 +6,21 @@ * Side Public License, v 1. */ -import { ComponentRegistry } from './component_registry'; +import type { + SectionRegistrySetup, + SectionRegistryStart, +} from '@kbn/management-settings-section-registry'; -const register = jest.fn(); -const get = jest.fn(); -const componentType = ComponentRegistry.componentType; +const addGlobalSection = jest.fn(); +const addSpaceSection = jest.fn(); +const getGlobalSections = jest.fn(); +const getSpacesSections = jest.fn(); export const advancedSettingsMock = { - createSetupContract() { - return { component: { register, componentType } }; + createSetupContract(): SectionRegistrySetup { + return { addGlobalSection, addSpaceSection }; }, - createStartContract() { - return { component: { get, componentType } }; + createStartContract(): SectionRegistryStart { + return { getGlobalSections, getSpacesSections }; }, }; diff --git a/src/plugins/advanced_settings/public/plugin.ts b/src/plugins/advanced_settings/public/plugin.ts index 2da7d76e0a4ff..f47993c1bb452 100644 --- a/src/plugins/advanced_settings/public/plugin.ts +++ b/src/plugins/advanced_settings/public/plugin.ts @@ -8,10 +8,10 @@ import { i18n } from '@kbn/i18n'; import { CoreSetup, Plugin } from '@kbn/core/public'; -import { ComponentRegistry } from './component_registry'; +import { SectionRegistry } from '@kbn/management-settings-section-registry'; import { AdvancedSettingsSetup, AdvancedSettingsStart, AdvancedSettingsPluginSetup } from './types'; -const component = new ComponentRegistry(); +const { setup: sectionRegistrySetup, start: sectionRegistryStart } = new SectionRegistry(); const title = i18n.translate('advancedSettings.advancedSettingsLabel', { defaultMessage: 'Advanced Settings', @@ -37,7 +37,7 @@ export class AdvancedSettingsPlugin return mountManagementSection( core.getStartServices, params, - component.start, + sectionRegistryStart, usageCollection ); }, @@ -59,13 +59,13 @@ export class AdvancedSettingsPlugin } return { - component: component.setup, + ...sectionRegistrySetup, }; } public start() { return { - component: component.start, + ...sectionRegistryStart, }; } } diff --git a/src/plugins/advanced_settings/public/types.ts b/src/plugins/advanced_settings/public/types.ts index 7d6647f0feef9..0dfff192e86b4 100644 --- a/src/plugins/advanced_settings/public/types.ts +++ b/src/plugins/advanced_settings/public/types.ts @@ -10,19 +10,16 @@ import { HomePublicPluginSetup } from '@kbn/home-plugin/public'; import { ManagementSetup } from '@kbn/management-plugin/public'; import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; -import { ComponentRegistry } from './component_registry'; +import type { + SectionRegistrySetup, + SectionRegistryStart, +} from '@kbn/management-settings-section-registry'; -export interface AdvancedSettingsSetup { - component: ComponentRegistry['setup']; -} -export interface AdvancedSettingsStart { - component: ComponentRegistry['start']; -} +export type AdvancedSettingsSetup = SectionRegistrySetup; +export type AdvancedSettingsStart = SectionRegistryStart; export interface AdvancedSettingsPluginSetup { management: ManagementSetup; home?: HomePublicPluginSetup; usageCollection?: UsageCollectionSetup; } - -export { ComponentRegistry }; diff --git a/src/plugins/advanced_settings/tsconfig.json b/src/plugins/advanced_settings/tsconfig.json index 83a33f3b3b1dd..be0f091423463 100644 --- a/src/plugins/advanced_settings/tsconfig.json +++ b/src/plugins/advanced_settings/tsconfig.json @@ -30,6 +30,8 @@ "@kbn/core-ui-settings-common", "@kbn/config-schema", "@kbn/core-plugins-server", + "@kbn/management-settings-section-registry", + "@kbn/react-kibana-context-render", ], "exclude": [ "target/**/*", diff --git a/src/plugins/controls/public/control_group/component/control_frame_component.tsx b/src/plugins/controls/public/control_group/component/control_frame_component.tsx index 6cb9a9b0cb970..babf862a05c9e 100644 --- a/src/plugins/controls/public/control_group/component/control_frame_component.tsx +++ b/src/plugins/controls/public/control_group/component/control_frame_component.tsx @@ -23,7 +23,6 @@ import { controlGroupSelector, useControlGroupContainer, } from '../embeddable/control_group_container'; -import { ControlGroupStrings } from '../control_group_strings'; import { useChildEmbeddable } from '../../hooks/use_child_embeddable'; import { ControlError } from './control_error_component'; @@ -135,11 +134,7 @@ export const ControlFrame = ({ {form} diff --git a/src/plugins/controls/public/control_group/control_group_strings.ts b/src/plugins/controls/public/control_group/control_group_strings.ts index fa66f1e593e48..6de3797b2e9c4 100644 --- a/src/plugins/controls/public/control_group/control_group_strings.ts +++ b/src/plugins/controls/public/control_group/control_group_strings.ts @@ -9,29 +9,6 @@ import { i18n } from '@kbn/i18n'; export const ControlGroupStrings = { - emptyState: { - getBadge: () => - i18n.translate('controls.controlGroup.emptyState.badgeText', { - defaultMessage: 'New', - }), - getCallToAction: () => - i18n.translate('controls.controlGroup.emptyState.callToAction', { - defaultMessage: - 'Filtering your data just got better with Controls, letting you display only the data you want to explore.', - }), - getAddControlButtonTitle: () => - i18n.translate('controls.controlGroup.emptyState.addControlButtonTitle', { - defaultMessage: 'Add control', - }), - getTwoLineLoadingTitle: () => - i18n.translate('controls.controlGroup.emptyState.twoLineLoadingTitle', { - defaultMessage: '...', - }), - getDismissButton: () => - i18n.translate('controls.controlGroup.emptyState.dismissButton', { - defaultMessage: 'Dismiss', - }), - }, manageControl: { getFlyoutCreateTitle: () => i18n.translate('controls.controlGroup.manageControl.createFlyoutTitle', { diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index 27fef2a017824..9822d2985bced 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -549,6 +549,10 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'observability:apmEnableProfilingIntegration': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, 'observability:apmEnableCriticalPath': { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index 81f7b0eb957f1..1b5f84019eb7b 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -150,6 +150,7 @@ export interface UsageStats { 'observability:apmServiceInventoryOptimizedSorting': boolean; 'observability:apmTraceExplorerTab': boolean; 'observability:apmEnableCriticalPath': boolean; + 'observability:apmEnableProfilingIntegration': boolean; 'securitySolution:enableGroupedNav': boolean; 'securitySolution:showRelatedIntegrations': boolean; 'visualization:visualize:legacyGaugeChartsLibrary': boolean; diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 00d8a96a59e0e..de4f1fae8f675 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -9870,6 +9870,12 @@ "description": "Non-default value of setting." } }, + "observability:apmEnableProfilingIntegration": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } + }, "observability:apmEnableCriticalPath": { "type": "boolean", "_meta": { diff --git a/src/plugins/telemetry_management_section/common/constants.ts b/src/plugins/telemetry_management_section/common/constants.ts new file mode 100644 index 0000000000000..8e825552fa081 --- /dev/null +++ b/src/plugins/telemetry_management_section/common/constants.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +/** + * These are the terms provided to Advanced Settings that map to this section. When searching, + * Advanced Settings will match against these terms to show or hide the section. + */ +export const SEARCH_TERMS: string[] = [ + 'telemetry', + 'usage data', // Keeping this term for BWC + 'usage collection', + i18n.translate('telemetry.telemetryConstant', { + defaultMessage: 'telemetry', + }), + i18n.translate('telemetry.usageCollectionConstant', { + defaultMessage: 'usage collection', + }), +].flatMap((term) => { + // Automatically lower-case and split by space the terms from above + const lowerCased = term.toLowerCase(); + return [lowerCased, ...lowerCased.split(' ')]; +}); diff --git a/src/plugins/advanced_settings/public/component_registry/page_footer/index.ts b/src/plugins/telemetry_management_section/common/index.ts similarity index 88% rename from src/plugins/advanced_settings/public/component_registry/page_footer/index.ts rename to src/plugins/telemetry_management_section/common/index.ts index 6a34aaa42dab1..a763ec8f6f0e3 100644 --- a/src/plugins/advanced_settings/public/component_registry/page_footer/index.ts +++ b/src/plugins/telemetry_management_section/common/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { PageFooter } from './page_footer'; +export { SEARCH_TERMS } from './constants'; diff --git a/src/plugins/telemetry_management_section/kibana.jsonc b/src/plugins/telemetry_management_section/kibana.jsonc index 2062c3aa3b0a6..0cd94f9d23234 100644 --- a/src/plugins/telemetry_management_section/kibana.jsonc +++ b/src/plugins/telemetry_management_section/kibana.jsonc @@ -7,8 +7,8 @@ "server": false, "browser": true, "requiredPlugins": [ - "advancedSettings", - "telemetry" + "telemetry", + "advancedSettings" ], "optionalPlugins": [ "usageCollection" @@ -17,4 +17,4 @@ "usageCollection" ] } -} +} \ No newline at end of file diff --git a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap index 57fa0b68affc8..515add8c29dd4 100644 --- a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap +++ b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap @@ -253,7 +253,6 @@ exports[`TelemetryManagementSectionComponent renders null because allowChangingO "timeZone": null, } } - onQueryMatchChange={[MockFunction]} showAppliesSettingMessage={true} telemetryService={ TelemetryService { diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx index a635f5668f52b..aa57485ce6049 100644 --- a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx +++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx @@ -22,7 +22,6 @@ describe('TelemetryManagementSectionComponent', () => { const coreSetup = coreMock.createSetup(); it('renders as expected', () => { - const onQueryMatchChange = jest.fn(); const telemetryService = new TelemetryService({ config: { appendServerlessChannelsSuffix: false, @@ -44,7 +43,6 @@ describe('TelemetryManagementSectionComponent', () => { shallowWithIntl( { }); it('renders null because query does not match the SEARCH_TERMS', () => { - const onQueryMatchChange = jest.fn(); const telemetryService = new TelemetryService({ config: { appendServerlessChannelsSuffix: false, @@ -77,7 +74,6 @@ describe('TelemetryManagementSectionComponent', () => { Fallback}> { component.rerender( Fallback}> { /> ); - expect(onQueryMatchChange).toHaveBeenCalledWith(false); - expect(onQueryMatchChange).toHaveBeenCalledTimes(1); } finally { component.unmount(); } }); it('renders because query matches the SEARCH_TERMS', () => { - const onQueryMatchChange = jest.fn(); const telemetryService = new TelemetryService({ config: { appendServerlessChannelsSuffix: false, @@ -129,7 +120,6 @@ describe('TelemetryManagementSectionComponent', () => { const component = mountWithIntl( { // It should also render if there is no query at all. expect(component.setProps({ ...component.props(), query: {} }).html()).not.toBe(''); - expect(onQueryMatchChange).toHaveBeenCalledWith(true); - - // Should only be called once because the second time does not change the result - expect(onQueryMatchChange).toHaveBeenCalledTimes(1); } finally { component.unmount(); } }); it('renders null because allowChangingOptInStatus is false', () => { - const onQueryMatchChange = jest.fn(); const telemetryService = new TelemetryService({ config: { appendServerlessChannelsSuffix: false, @@ -176,7 +161,6 @@ describe('TelemetryManagementSectionComponent', () => { const component = mountWithIntl( { try { expect(component).toMatchSnapshot(); component.setProps({ ...component.props(), query: { text: 'TeLEMetry' } }); - expect(onQueryMatchChange).toHaveBeenCalledWith(false); } finally { component.unmount(); } }); it('shows the OptInExampleFlyout', () => { - const onQueryMatchChange = jest.fn(); const telemetryService = new TelemetryService({ config: { appendServerlessChannelsSuffix: false, @@ -214,7 +196,6 @@ describe('TelemetryManagementSectionComponent', () => { const component = mountWithIntl( { }); it('toggles the OptIn button', async () => { - const onQueryMatchChange = jest.fn(); const telemetryService = new TelemetryService({ config: { appendServerlessChannelsSuffix: false, @@ -256,7 +236,6 @@ describe('TelemetryManagementSectionComponent', () => { const component = mountWithIntl( { }); it('test the wrapper (for coverage purposes)', () => { - const onQueryMatchChange = jest.fn(); const telemetryService = new TelemetryService({ config: { appendServerlessChannelsSuffix: false, @@ -307,7 +285,6 @@ describe('TelemetryManagementSectionComponent', () => { { - // Automatically lower-case and split by space the terms from above - const lowerCased = term.toLowerCase(); - return [lowerCased, ...lowerCased.split(' ')]; -}); - interface Props { telemetryService: TelemetryService; - onQueryMatchChange: (searchTermMatches: boolean) => void; showAppliesSettingMessage: boolean; enableSaving: boolean; - query?: { text: string }; toasts: ToastsStart; docLinks: DocLinksStart['links']; } @@ -57,7 +39,6 @@ interface State { processing: boolean; showExample: boolean; showSecurityExample: boolean; - queryMatches: boolean | null; enabled: boolean; } @@ -69,47 +50,18 @@ export class TelemetryManagementSection extends Component { processing: false, showExample: false, showSecurityExample: false, - queryMatches: props.query ? this.checkQueryMatch(props.query) : null, enabled: this.props.telemetryService.getIsOptedIn() || false, }; } - UNSAFE_componentWillReceiveProps(nextProps: Props) { - const { query } = nextProps; - const queryMatches = this.checkQueryMatch(query); - - if (queryMatches !== this.state.queryMatches) { - this.setState( - { - queryMatches, - }, - () => { - this.props.onQueryMatchChange(queryMatches); - } - ); - } - } - - checkQueryMatch(query?: { text: string }): boolean { - const searchTerm = (query?.text ?? '').toLowerCase(); - return ( - this.props.telemetryService.getCanChangeOptInStatus() && - SEARCH_TERMS.some((term) => term.indexOf(searchTerm) >= 0) - ); - } - render() { const { telemetryService } = this.props; - const { showExample, queryMatches, enabled, processing } = this.state; + const { showExample, enabled, processing } = this.state; if (!telemetryService.getCanChangeOptInStatus()) { return null; } - if (queryMatches !== null && !queryMatches) { - return null; - } - return ( {showExample && ( diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx index 75cb161df926f..9ebb3e83fc7b9 100644 --- a/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx +++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx @@ -10,6 +10,7 @@ import React, { lazy, Suspense } from 'react'; import { EuiLoadingSpinner } from '@elastic/eui'; import type { TelemetryPluginSetup } from '@kbn/telemetry-plugin/public'; import { DocLinksStart } from '@kbn/core/public'; +import { RegistryComponentProps } from '@kbn/management-settings-section-registry'; import type TelemetryManagementSection from './telemetry_management_section'; export type TelemetryManagementSectionWrapperProps = Omit< @@ -23,12 +24,16 @@ export function telemetryManagementSectionWrapper( telemetryService: TelemetryPluginSetup['telemetryService'], docLinks: DocLinksStart['links'] ) { - const TelemetryManagementSectionWrapper = (props: TelemetryManagementSectionWrapperProps) => ( + const TelemetryManagementSectionWrapper = ({ + enableSaving, + ...props + }: RegistryComponentProps) => ( }> diff --git a/src/plugins/telemetry_management_section/public/plugin.tsx b/src/plugins/telemetry_management_section/public/plugin.tsx index 8595a39615c69..45707b55b4d79 100644 --- a/src/plugins/telemetry_management_section/public/plugin.tsx +++ b/src/plugins/telemetry_management_section/public/plugin.tsx @@ -12,10 +12,8 @@ import type { TelemetryPluginSetup } from '@kbn/telemetry-plugin/public'; import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import type { CoreStart, CoreSetup, DocLinksStart } from '@kbn/core/public'; -import { - telemetryManagementSectionWrapper, - TelemetryManagementSectionWrapperProps, -} from './components/telemetry_management_section_wrapper'; +import { telemetryManagementSectionWrapper } from './components/telemetry_management_section_wrapper'; +import { SEARCH_TERMS } from '../common'; export interface TelemetryManagementSectionPluginDepsSetup { telemetry: TelemetryPluginSetup; @@ -40,20 +38,22 @@ export class TelemetryManagementSectionPlugin { const ApplicationUsageTrackingProvider = usageCollection?.components.ApplicationUsageTrackingProvider ?? React.Fragment; - advancedSettings.component.register( - advancedSettings.component.componentType.PAGE_FOOTER_COMPONENT, - (props) => { - return ( - - {telemetryManagementSectionWrapper( - telemetryService, - docLinksLinks - )(props as TelemetryManagementSectionWrapperProps)} - - ); - }, - true - ); + + const queryMatch = (query: string) => { + const searchTerm = query.toLowerCase(); + return ( + telemetryService.getCanChangeOptInStatus() && + SEARCH_TERMS.some((term) => term.indexOf(searchTerm) >= 0) + ); + }; + + advancedSettings.addGlobalSection((props) => { + return ( + + {telemetryManagementSectionWrapper(telemetryService, docLinksLinks)(props)} + + ); + }, queryMatch); return {}; } diff --git a/src/plugins/telemetry_management_section/tsconfig.json b/src/plugins/telemetry_management_section/tsconfig.json index ebdad6eb86612..76e8beadc68b2 100644 --- a/src/plugins/telemetry_management_section/tsconfig.json +++ b/src/plugins/telemetry_management_section/tsconfig.json @@ -5,6 +5,7 @@ "isolatedModules": true }, "include": [ + "common/**/*", "public/**/*", "../../../typings/**/*" ], @@ -16,6 +17,7 @@ "@kbn/test-jest-helpers", "@kbn/i18n-react", "@kbn/i18n", + "@kbn/management-settings-section-registry", ], "exclude": [ "target/**/*", diff --git a/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx b/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx index 5c31c08f46853..30bb05c7b43d6 100644 --- a/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx +++ b/src/plugins/visualizations/public/visualize_app/components/visualize_listing.tsx @@ -378,8 +378,10 @@ export const VisualizeListing = () => { entityNamePlural={i18n.translate('visualizations.listing.table.entityNamePlural', { defaultMessage: 'visualizations', })} - getDetailViewLink={({ attributes: { editApp, editUrl, error } }) => - getVisualizeListItemLink(application, kbnUrlStateStorage, editApp, editUrl, error) + getDetailViewLink={({ attributes: { editApp, editUrl, error, readOnly } }) => + readOnly + ? undefined + : getVisualizeListItemLink(application, kbnUrlStateStorage, editApp, editUrl, error) } tableCaption={visualizeLibraryTitle} {...tableViewProps} diff --git a/test/functional/apps/console/_console_ccs.ts b/test/functional/apps/console/_console_ccs.ts index 486223f02d320..8778c2e6e70bb 100644 --- a/test/functional/apps/console/_console_ccs.ts +++ b/test/functional/apps/console/_console_ccs.ts @@ -32,8 +32,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await remoteEsArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); }); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/163365 - describe.skip('Perform CCS Search in Console', () => { + describe('Perform CCS Search in Console', () => { before(async () => { await PageObjects.console.clearTextArea(); }); @@ -44,7 +43,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.console.clickPlay(); await retry.try(async () => { const actualResponse = await PageObjects.console.getResponse(); - expect(actualResponse).to.contain('"extension": "jpg",'); + expect(actualResponse).to.contain('"_index": "ftr-remote:logstash-2015.09.20"'); }); }); }); diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 66a2e385d3e6c..c0573c10c10b1 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -240,6 +240,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.ilm.ui.enabled (boolean)', 'xpack.index_management.ui.enabled (boolean)', 'xpack.index_management.enableIndexActions (any)', + 'xpack.index_management.enableLegacyTemplates (any)', 'xpack.infra.sources.default.fields.message (array)', /** * xpack.infra.logs is conditional and will resolve to an object of properties diff --git a/test/server_integration/http/platform/cache.ts b/test/server_integration/http/platform/cache.ts index 2c1aa90e963e2..6e1cd8ab39db0 100644 --- a/test/server_integration/http/platform/cache.ts +++ b/test/server_integration/http/platform/cache.ts @@ -7,7 +7,7 @@ */ import { FtrProviderContext } from '../../services/types'; -// eslint-disable-next-line import/no-default-export + export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); diff --git a/test/server_integration/http/platform/config.status.ts b/test/server_integration/http/platform/config.status.ts index 456756aa79262..638d850a8f75c 100644 --- a/test/server_integration/http/platform/config.status.ts +++ b/test/server_integration/http/platform/config.status.ts @@ -17,7 +17,6 @@ import { FtrConfigProviderContext, findTestPluginPaths } from '@kbn/test'; * and installing plugins against built Kibana. This test must be run against source only in order to build the * fixture plugins */ -// eslint-disable-next-line import/no-default-export export default async function ({ readConfigFile }: FtrConfigProviderContext) { const httpConfig = await readConfigFile(require.resolve('../../config.base.js')); diff --git a/test/server_integration/http/platform/config.ts b/test/server_integration/http/platform/config.ts index 028ff67b43022..e78525cb8da60 100644 --- a/test/server_integration/http/platform/config.ts +++ b/test/server_integration/http/platform/config.ts @@ -8,7 +8,6 @@ import { FtrConfigProviderContext } from '@kbn/test'; -// eslint-disable-next-line import/no-default-export export default async function ({ readConfigFile }: FtrConfigProviderContext) { const httpConfig = await readConfigFile(require.resolve('../../config.base.js')); diff --git a/test/server_integration/http/platform/headers.ts b/test/server_integration/http/platform/headers.ts index 309dfbc71b5ff..1a8e9fd610679 100644 --- a/test/server_integration/http/platform/headers.ts +++ b/test/server_integration/http/platform/headers.ts @@ -14,7 +14,6 @@ import { FtrProviderContext } from '../../services/types'; const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const oneSec = 1_000; -// eslint-disable-next-line import/no-default-export export default function ({ getService }: FtrProviderContext) { const config = getService('config'); diff --git a/test/server_integration/http/platform/status.ts b/test/server_integration/http/platform/status.ts index 48006576128ce..50c136bfa1027 100644 --- a/test/server_integration/http/platform/status.ts +++ b/test/server_integration/http/platform/status.ts @@ -12,7 +12,6 @@ import { FtrProviderContext } from '../../services/types'; type ServiceStatusSerialized = Omit & { level: string }; -// eslint-disable-next-line import/no-default-export export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const retry = getService('retry'); diff --git a/test/server_integration/http/ssl_redirect/config.js b/test/server_integration/http/ssl_redirect/config.ts similarity index 93% rename from test/server_integration/http/ssl_redirect/config.js rename to test/server_integration/http/ssl_redirect/config.ts index 47568b16bf6ba..8f8db0e9aae0e 100644 --- a/test/server_integration/http/ssl_redirect/config.js +++ b/test/server_integration/http/ssl_redirect/config.ts @@ -9,10 +9,11 @@ import Url from 'url'; import { readFileSync } from 'fs'; import { CA_CERT_PATH, KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; +import { FtrConfigProviderContext } from '@kbn/test'; import { createKibanaSupertestProvider } from '../../services'; -export default async function ({ readConfigFile }) { +export default async function ({ readConfigFile }: FtrConfigProviderContext) { const httpConfig = await readConfigFile(require.resolve('../../config.base.js')); const certificateAuthorities = [readFileSync(CA_CERT_PATH)]; diff --git a/test/server_integration/http/ssl_redirect/index.js b/test/server_integration/http/ssl_redirect/index.ts similarity index 65% rename from test/server_integration/http/ssl_redirect/index.js rename to test/server_integration/http/ssl_redirect/index.ts index 07ae0eb4bb565..6e4e7cfb7decf 100644 --- a/test/server_integration/http/ssl_redirect/index.js +++ b/test/server_integration/http/ssl_redirect/index.ts @@ -6,19 +6,23 @@ * Side Public License, v 1. */ -export default function ({ getService }) { +import { FtrProviderContext } from '../../services/types'; + +export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - // Failing: See https://github.com/elastic/kibana/issues/131192 - // Failing: See https://github.com/elastic/kibana/issues/131192 - describe.skip('kibana server with ssl', () => { + describe('kibana server with ssl', () => { it('redirects http requests at redirect port to https', async () => { const host = process.env.TEST_KIBANA_HOST || 'localhost'; const port = process.env.TEST_KIBANA_PORT || '5620'; const url = `https://${host}:${port}/`; await supertest.get('/').expect('location', url).expect(302); + }); + // Skips because the current version of supertest cannot follow redirects + // Can be unskipped once https://github.com/elastic/kibana/pull/163716 is merged + it.skip('does not boot-loop (2nd redirect points to the landing page)', async () => { await supertest.get('/').redirects(1).expect('location', '/spaces/enter').expect(302); }); }); diff --git a/tsconfig.base.json b/tsconfig.base.json index cc2a1a7d3efb2..dd6a27a32dfa9 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -950,6 +950,8 @@ "@kbn/management-cards-navigation/*": ["packages/kbn-management/cards_navigation/*"], "@kbn/management-plugin": ["src/plugins/management"], "@kbn/management-plugin/*": ["src/plugins/management/*"], + "@kbn/management-settings-section-registry": ["packages/kbn-management/settings/section_registry"], + "@kbn/management-settings-section-registry/*": ["packages/kbn-management/settings/section_registry/*"], "@kbn/management-storybook-config": ["packages/kbn-management/storybook/config"], "@kbn/management-storybook-config/*": ["packages/kbn-management/storybook/config/*"], "@kbn/management-test-plugin": ["test/plugin_functional/plugins/management_test_plugin"], @@ -1562,7 +1564,9 @@ "@kbn/yarn-lock-validator/*": ["packages/kbn-yarn-lock-validator/*"], // END AUTOMATED PACKAGE LISTING // Allows for importing from `kibana` package for the exported types. - "@emotion/core": ["typings/@emotion"], + "@emotion/core": [ + "typings/@emotion" + ], }, // Support .tsx files and transform JSX into calls to React.createElement "jsx": "react", @@ -1635,4 +1639,4 @@ "@kbn/ambient-storybook-types" ] } -} +} \ No newline at end of file diff --git a/x-pack/plugins/apm/public/components/app/settings/general_settings/index.tsx b/x-pack/plugins/apm/public/components/app/settings/general_settings/index.tsx index a39c86be9de12..52ede89eefba4 100644 --- a/x-pack/plugins/apm/public/components/app/settings/general_settings/index.tsx +++ b/x-pack/plugins/apm/public/components/app/settings/general_settings/index.tsx @@ -19,6 +19,7 @@ import { apmEnableServiceMetrics, apmEnableContinuousRollups, enableAgentExplorerView, + apmEnableProfilingIntegration, } from '@kbn/observability-plugin/common'; import { isEmpty } from 'lodash'; import React from 'react'; @@ -37,6 +38,7 @@ const apmSettingsKeys = [ apmEnableServiceMetrics, apmEnableContinuousRollups, enableAgentExplorerView, + apmEnableProfilingIntegration, ]; export function GeneralSettings() { diff --git a/x-pack/plugins/apm/public/hooks/use_profiling_plugin.tsx b/x-pack/plugins/apm/public/hooks/use_profiling_plugin.tsx index ffa07be6bfc12..945bbc43fe3a2 100644 --- a/x-pack/plugins/apm/public/hooks/use_profiling_plugin.tsx +++ b/x-pack/plugins/apm/public/hooks/use_profiling_plugin.tsx @@ -6,10 +6,15 @@ */ import { useEffect, useState } from 'react'; +import { apmEnableProfilingIntegration } from '@kbn/observability-plugin/common'; import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context'; export function useProfilingPlugin() { - const { plugins } = useApmPluginContext(); + const { plugins, core } = useApmPluginContext(); + const isProfilingIntegrationEnabled = core.uiSettings.get( + apmEnableProfilingIntegration, + false + ); const [isProfilingPluginInitialized, setIsProfilingPluginInitialized] = useState(); @@ -28,8 +33,10 @@ export function useProfilingPlugin() { return { isProfilingPluginInitialized, - profilingLocators: isProfilingPluginInitialized - ? plugins.profiling?.locators - : undefined, + profilingLocators: + isProfilingIntegrationEnabled && isProfilingPluginInitialized + ? plugins.profiling?.locators + : undefined, + isProfilingIntegrationEnabled, }; } diff --git a/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_service_summary/index.ts b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_service_summary/index.ts index fa2febe3c90db..92eafdf0b4ee7 100644 --- a/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_service_summary/index.ts +++ b/x-pack/plugins/apm/server/routes/assistant_functions/get_apm_service_summary/index.ts @@ -213,7 +213,7 @@ async function getAnomalies({ export interface ServiceSummary { 'service.name': string; - 'service.environment': Environment[]; + 'service.environment': string[]; 'agent.name'?: string; 'service.version'?: string[]; 'language.name'?: string; @@ -298,15 +298,18 @@ export async function getApmServiceSummary({ }), apmAlertsClient.search({ size: 100, - query: { - bool: { - filter: [ - ...termQuery(ALERT_RULE_PRODUCER, 'apm'), - ...termQuery(ALERT_STATUS, ALERT_STATUS_ACTIVE), - ...rangeQuery(start, end), - ...termQuery(SERVICE_NAME, serviceName), - ...environmentQuery(environment), - ], + track_total_hits: false, + body: { + query: { + bool: { + filter: [ + ...termQuery(ALERT_RULE_PRODUCER, 'apm'), + ...termQuery(ALERT_STATUS, ALERT_STATUS_ACTIVE), + ...rangeQuery(start, end), + ...termQuery(SERVICE_NAME, serviceName), + ...environmentQuery(environment), + ], + }, }, }, }), diff --git a/x-pack/plugins/canvas/server/config.test.ts b/x-pack/plugins/canvas/server/config.test.ts deleted file mode 100644 index 4c66c718d1b2d..0000000000000 --- a/x-pack/plugins/canvas/server/config.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -jest.mock('crypto', () => ({ - randomBytes: jest.fn(), - constants: jest.requireActual('crypto').constants, -})); - -jest.mock('@kbn/utils', () => ({ - getLogsPath: () => '/mock/kibana/logs/path', -})); - -import { ConfigSchema } from './config'; - -describe('config schema', () => { - it('generates proper defaults', () => { - expect(ConfigSchema.validate({})).toMatchInlineSnapshot(` - Object { - "enabled": true, - } - `); - - expect(ConfigSchema.validate({}, { dev: false })).toMatchInlineSnapshot(` - Object { - "enabled": true, - } - `); - - expect(ConfigSchema.validate({}, { dev: true })).toMatchInlineSnapshot(` - Object { - "enabled": true, - } - `); - }); - - it('should throw error if spaces is disabled', () => { - expect(() => ConfigSchema.validate({ enabled: false })).toThrow( - '[enabled]: Canvas can only be disabled in development mode' - ); - - expect(() => ConfigSchema.validate({ enabled: false }, { dev: false })).toThrow( - '[enabled]: Canvas can only be disabled in development mode' - ); - }); - - it('should not throw error if spaces is disabled in development mode', () => { - expect(() => ConfigSchema.validate({ enabled: false }, { dev: true })).not.toThrow(); - }); -}); diff --git a/x-pack/plugins/canvas/server/config.ts b/x-pack/plugins/canvas/server/config.ts index 6cbcff6930d88..89ed9b3f74314 100644 --- a/x-pack/plugins/canvas/server/config.ts +++ b/x-pack/plugins/canvas/server/config.ts @@ -8,17 +8,5 @@ import { schema } from '@kbn/config-schema'; export const ConfigSchema = schema.object({ - enabled: schema.conditional( - schema.contextRef('dev'), - true, - schema.boolean({ defaultValue: true }), - schema.boolean({ - validate: (rawValue) => { - if (rawValue === false) { - return 'Canvas can only be disabled in development mode'; - } - }, - defaultValue: true, - }) - ), + enabled: schema.boolean({ defaultValue: true }), }); diff --git a/x-pack/plugins/index_management/README.md b/x-pack/plugins/index_management/README.md index fba162259ce91..b50309ac36099 100644 --- a/x-pack/plugins/index_management/README.md +++ b/x-pack/plugins/index_management/README.md @@ -53,7 +53,7 @@ POST %25%7B%5B%40metadata%5D%5Bbeat%5D%7D-%25%7B%5B%40metadata%5D%5Bversion%5D%7 ### Quick steps for testing -By default, **legacy index templates** are not shown in the UI. Make them appear by creating one in Console: +**Legacy index templates** are only shown in the UI on stateful *and* if a user has existing legacy index templates. You can test this functionality by creating one in Console: ``` PUT _template/template_1 @@ -62,6 +62,8 @@ PUT _template/template_1 } ``` +On serverless, Elasticsearch does not support legacy index templates and therefore this functionality is disabled in Kibana via the config `xpack.index_management.enableLegacyTemplates`. For more details, see [#163518](https://github.com/elastic/kibana/pull/163518). + To test **Cloud-managed templates**: 1. Add `cluster.metadata.managed_index_templates` setting via Dev Tools: diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx index 67fa2d99787ba..b1dd1d748f309 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx @@ -56,6 +56,11 @@ const appDependencies = { executionContext: executionContextServiceMock.createStartContract(), }, plugins: {}, + // Default stateful configuration + config: { + enableLegacyTemplates: true, + enableIndexActions: true, + }, } as any; export const kibanaVersion = new SemVer(MAJOR_VERSION); @@ -82,7 +87,6 @@ export const WithAppDependencies = (props: any) => { httpService.setup(httpSetup); const mergedDependencies = merge({}, appDependencies, overridingDependencies); - return ( diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts index 8b2b1d6568253..16a3e1fd09bbd 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts @@ -225,11 +225,11 @@ describe('', () => { ]); httpRequestsMockHelpers.setReloadIndicesResponse({ indexNames: [indexNameA, indexNameB] }); - testBed = await setup(httpSetup, { - enableIndexActions: true, + await act(async () => { + testBed = await setup(httpSetup); }); - const { component, find } = testBed; + const { component, find } = testBed; component.update(); find('indexTableIndexNameLink').at(0).simulate('click'); @@ -270,8 +270,8 @@ describe('', () => { }); test('should be able to open a closed index', async () => { - testBed = await setup(httpSetup, { - enableIndexActions: true, + await act(async () => { + testBed = await setup(httpSetup); }); const { component, find, actions } = testBed; diff --git a/x-pack/plugins/index_management/__jest__/components/index_table.test.js b/x-pack/plugins/index_management/__jest__/components/index_table.test.js index ad704311dd210..5d8371010b3fe 100644 --- a/x-pack/plugins/index_management/__jest__/components/index_table.test.js +++ b/x-pack/plugins/index_management/__jest__/components/index_table.test.js @@ -168,7 +168,11 @@ describe('index table', () => { }, plugins: {}, url: urlServiceMock, - enableIndexActions: true, + // Default stateful configuration + config: { + enableLegacyTemplates: true, + enableIndexActions: true, + }, }; component = ( @@ -515,8 +519,8 @@ describe('index table', () => { describe('Common index actions', () => { beforeEach(() => { - // Mock initialization of services - setupMockComponent({ enableIndexActions: false }); + // Mock initialization of services; set enableIndexActions=false to verify config behavior + setupMockComponent({ config: { enableIndexActions: false, enableLegacyTemplates: true } }); }); test('Common index actions should be hidden when feature is turned off', async () => { diff --git a/x-pack/plugins/index_management/common/constants/api_base_path.ts b/x-pack/plugins/index_management/common/constants/api_base_path.ts index c923913bb9d83..c111d95dab192 100644 --- a/x-pack/plugins/index_management/common/constants/api_base_path.ts +++ b/x-pack/plugins/index_management/common/constants/api_base_path.ts @@ -6,3 +6,5 @@ */ export const API_BASE_PATH = '/api/index_management'; + +export const INTERNAL_API_BASE_PATH = '/internal/index_management'; diff --git a/x-pack/plugins/index_management/common/constants/index.ts b/x-pack/plugins/index_management/common/constants/index.ts index 6641e6ef67c7d..786dad4a5e375 100644 --- a/x-pack/plugins/index_management/common/constants/index.ts +++ b/x-pack/plugins/index_management/common/constants/index.ts @@ -6,7 +6,7 @@ */ export { BASE_PATH } from './base_path'; -export { API_BASE_PATH } from './api_base_path'; +export { API_BASE_PATH, INTERNAL_API_BASE_PATH } from './api_base_path'; export { INVALID_INDEX_PATTERN_CHARS, INVALID_TEMPLATE_NAME_CHARS } from './invalid_characters'; export * from './index_statuses'; diff --git a/x-pack/plugins/index_management/common/index.ts b/x-pack/plugins/index_management/common/index.ts index 127123609b186..a481d17615d8d 100644 --- a/x-pack/plugins/index_management/common/index.ts +++ b/x-pack/plugins/index_management/common/index.ts @@ -8,7 +8,7 @@ // TODO: https://github.com/elastic/kibana/issues/110892 /* eslint-disable @kbn/eslint/no_export_all */ -export { API_BASE_PATH, BASE_PATH, MAJOR_VERSION } from './constants'; +export { API_BASE_PATH, INTERNAL_API_BASE_PATH, BASE_PATH, MAJOR_VERSION } from './constants'; export { getTemplateParameter } from './lib'; diff --git a/x-pack/plugins/index_management/common/types/enrich_policies.ts b/x-pack/plugins/index_management/common/types/enrich_policies.ts new file mode 100644 index 0000000000000..4688cb41135f1 --- /dev/null +++ b/x-pack/plugins/index_management/common/types/enrich_policies.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { EnrichPolicyType } from '@elastic/elasticsearch/lib/api/types'; + +export interface SerializedEnrichPolicy { + type: EnrichPolicyType; + name: string; + sourceIndices: string[]; + matchField: string; + enrichFields: string[]; +} diff --git a/x-pack/plugins/index_management/common/types/index.ts b/x-pack/plugins/index_management/common/types/index.ts index 0cc514b47024f..ce5d96a842366 100644 --- a/x-pack/plugins/index_management/common/types/index.ts +++ b/x-pack/plugins/index_management/common/types/index.ts @@ -16,3 +16,5 @@ export * from './templates'; export type { DataStreamFromEs, Health, DataStream, DataStreamIndex } from './data_streams'; export * from './component_templates'; + +export * from './enrich_policies'; diff --git a/x-pack/plugins/index_management/public/application/app_context.tsx b/x-pack/plugins/index_management/public/application/app_context.tsx index 9acbda3f9685f..eb52f50d62ecf 100644 --- a/x-pack/plugins/index_management/public/application/app_context.tsx +++ b/x-pack/plugins/index_management/public/application/app_context.tsx @@ -44,6 +44,10 @@ export interface AppDependencies { httpService: HttpService; notificationService: NotificationService; }; + config: { + enableIndexActions: boolean; + enableLegacyTemplates: boolean; + }; history: ScopedHistory; setBreadcrumbs: ManagementAppMountParams['setBreadcrumbs']; uiSettings: IUiSettingsClient; @@ -52,7 +56,6 @@ export interface AppDependencies { docLinks: DocLinksStart; kibanaVersion: SemVer; theme$: Observable; - enableIndexActions: boolean; } export const AppContextProvider = ({ diff --git a/x-pack/plugins/index_management/public/application/mount_management_section.ts b/x-pack/plugins/index_management/public/application/mount_management_section.ts index 6bb3b834ce85f..997568a2eb69a 100644 --- a/x-pack/plugins/index_management/public/application/mount_management_section.ts +++ b/x-pack/plugins/index_management/public/application/mount_management_section.ts @@ -53,7 +53,8 @@ export async function mountManagementSection( extensionsService: ExtensionsService, isFleetEnabled: boolean, kibanaVersion: SemVer, - enableIndexActions: boolean = true + enableIndexActions: boolean = true, + enableLegacyTemplates: boolean = true ) { const { element, setBreadcrumbs, history, theme$ } = params; const [core, startDependencies] = await coreSetup.getStartServices(); @@ -95,7 +96,10 @@ export async function mountManagementSection( uiMetricService, extensionsService, }, - enableIndexActions, + config: { + enableIndexActions, + enableLegacyTemplates, + }, history, setBreadcrumbs, uiSettings, diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js index 4188797431e5d..4dd22c0a73e13 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/index_actions_context_menu/index_actions_context_menu.js @@ -49,7 +49,9 @@ export class IndexActionsContextMenu extends Component { this.setState({ isActionConfirmed }); }; panels({ services: { extensionsService }, core: { getUrlForApp } }) { - const { enableIndexActions } = this.context; + const { + config: { enableIndexActions }, + } = this.context; const { closeIndices, diff --git a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx index 3a49237a517c9..eff5cbb554904 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_clone/template_clone.tsx @@ -19,6 +19,7 @@ import { getTemplateDetailsLink } from '../../services/routing'; import { saveTemplate, useLoadIndexTemplate } from '../../services/api'; import { getIsLegacyFromQueryParams } from '../../lib/index_templates'; import { attemptToURIDecode } from '../../../shared_imports'; +import { useAppContext } from '../../app_context'; interface MatchParams { name: string; @@ -32,7 +33,11 @@ export const TemplateClone: React.FunctionComponent { const decodedTemplateName = attemptToURIDecode(name)!; - const isLegacy = getIsLegacyFromQueryParams(location); + const { + config: { enableLegacyTemplates }, + } = useAppContext(); + // We don't expect the `legacy` query to be used when legacy templates are disabled, however, we add the `enableLegacyTemplates` check as a safeguard + const isLegacy = enableLegacyTemplates && getIsLegacyFromQueryParams(location); const [isSaving, setIsSaving] = useState(false); const [saveError, setSaveError] = useState(null); diff --git a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx index cb8f29d222d63..e5422ca93db26 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_create/template_create.tsx @@ -18,12 +18,17 @@ import { TemplateForm } from '../../components'; import { breadcrumbService } from '../../services/breadcrumbs'; import { saveTemplate } from '../../services/api'; import { getTemplateDetailsLink } from '../../services/routing'; +import { useAppContext } from '../../app_context'; export const TemplateCreate: React.FunctionComponent = ({ history }) => { const [isSaving, setIsSaving] = useState(false); const [saveError, setSaveError] = useState(null); + const { + config: { enableLegacyTemplates }, + } = useAppContext(); const search = parse(useLocation().search.substring(1)); - const isLegacy = Boolean(search.legacy); + // We don't expect the `legacy` query to be used when legacy templates are disabled, however, we add the `enableLegacyTemplates` check as a safeguard + const isLegacy = enableLegacyTemplates && Boolean(search.legacy); const onSave = async (template: TemplateDeserialized) => { const { name } = template; diff --git a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx index c96502fd15066..b0a6b95351386 100644 --- a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx +++ b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx @@ -23,6 +23,7 @@ import { useLoadIndexTemplate, updateTemplate } from '../../services/api'; import { getTemplateDetailsLink } from '../../services/routing'; import { TemplateForm } from '../../components'; import { getIsLegacyFromQueryParams } from '../../lib/index_templates'; +import { useAppContext } from '../../app_context'; interface MatchParams { name: string; @@ -36,7 +37,12 @@ export const TemplateEdit: React.FunctionComponent { const decodedTemplateName = attemptToURIDecode(name)!; - const isLegacy = getIsLegacyFromQueryParams(location); + const { + config: { enableLegacyTemplates }, + } = useAppContext(); + + // We don't expect the `legacy` query to be used when legacy templates are disabled, however, we add the enableLegacyTemplates check as a safeguard + const isLegacy = enableLegacyTemplates && getIsLegacyFromQueryParams(location); const [isSaving, setIsSaving] = useState(false); const [saveError, setSaveError] = useState(null); diff --git a/x-pack/plugins/index_management/public/mocks.ts b/x-pack/plugins/index_management/public/mocks.ts index 30e21c80be5b1..69a43b985787a 100644 --- a/x-pack/plugins/index_management/public/mocks.ts +++ b/x-pack/plugins/index_management/public/mocks.ts @@ -6,12 +6,15 @@ */ import { extensionsServiceMock } from './services/extensions_service.mock'; +import { publicApiServiceMock } from './services/public_api_service.mock'; export { extensionsServiceMock } from './services/extensions_service.mock'; +export { publicApiServiceMock } from './services/public_api_service.mock'; function createIdxManagementSetupMock() { const mock = { extensionsService: extensionsServiceMock, + publicApiService: publicApiServiceMock, }; return mock; diff --git a/x-pack/plugins/index_management/public/plugin.ts b/x-pack/plugins/index_management/public/plugin.ts index fc965a061e0bf..0771e254fd6aa 100644 --- a/x-pack/plugins/index_management/public/plugin.ts +++ b/x-pack/plugins/index_management/public/plugin.ts @@ -11,7 +11,7 @@ import SemVer from 'semver/classes/semver'; import { CoreSetup, PluginInitializerContext } from '@kbn/core/public'; import { setExtensionsService } from './application/store/selectors/extension_service'; -import { ExtensionsService } from './services'; +import { ExtensionsService, PublicApiService } from './services'; import { IndexManagementPluginSetup, @@ -39,6 +39,7 @@ export class IndexMgmtUIPlugin { const { ui: { enabled: isIndexManagementUiEnabled }, enableIndexActions, + enableLegacyTemplates, } = this.ctx.config.get(); if (isIndexManagementUiEnabled) { @@ -57,13 +58,15 @@ export class IndexMgmtUIPlugin { this.extensionsService, Boolean(fleet), kibanaVersion, - enableIndexActions + enableIndexActions, + enableLegacyTemplates ); }, }); } return { + apiService: new PublicApiService(coreSetup.http), extensionsService: this.extensionsService.setup(), }; } diff --git a/x-pack/plugins/index_management/public/services/index.ts b/x-pack/plugins/index_management/public/services/index.ts index f32787a427b89..8f4ddbeffba35 100644 --- a/x-pack/plugins/index_management/public/services/index.ts +++ b/x-pack/plugins/index_management/public/services/index.ts @@ -7,3 +7,6 @@ export type { ExtensionsSetup } from './extensions_service'; export { ExtensionsService } from './extensions_service'; + +export type { PublicApiServiceSetup } from './public_api_service'; +export { PublicApiService } from './public_api_service'; diff --git a/x-pack/plugins/index_management/public/services/public_api_service.mock.ts b/x-pack/plugins/index_management/public/services/public_api_service.mock.ts new file mode 100644 index 0000000000000..85ce1b232c06a --- /dev/null +++ b/x-pack/plugins/index_management/public/services/public_api_service.mock.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PublicApiServiceSetup } from './public_api_service'; + +export type PublicApiServiceSetupMock = jest.Mocked; + +const createServiceMock = (): PublicApiServiceSetupMock => ({ + getAllEnrichPolicies: jest.fn(), +}); + +export const publicApiServiceMock = { + createSetupContract: createServiceMock, +}; diff --git a/x-pack/plugins/index_management/public/services/public_api_service.ts b/x-pack/plugins/index_management/public/services/public_api_service.ts new file mode 100644 index 0000000000000..33d43b9304fdb --- /dev/null +++ b/x-pack/plugins/index_management/public/services/public_api_service.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 { HttpSetup } from '@kbn/core/public'; +import { sendRequest, SendRequestResponse } from '../shared_imports'; +import { API_BASE_PATH } from '../../common/constants'; +import { SerializedEnrichPolicy } from '../../common/types'; + +export interface PublicApiServiceSetup { + getAllEnrichPolicies(): Promise>; +} + +/** + * Index Management public API service + */ +export class PublicApiService { + private http: HttpSetup; + + /** + * constructor + * @param http http dependency + */ + constructor(http: HttpSetup) { + this.http = http; + } + + /** + * Gets a list of all the enrich policies + */ + getAllEnrichPolicies() { + return sendRequest(this.http, { + path: `${API_BASE_PATH}/enrich_policies`, + method: 'get', + }); + } +} diff --git a/x-pack/plugins/index_management/public/types.ts b/x-pack/plugins/index_management/public/types.ts index 20d2405a0fa4b..b3e479b081fb4 100644 --- a/x-pack/plugins/index_management/public/types.ts +++ b/x-pack/plugins/index_management/public/types.ts @@ -8,9 +8,10 @@ import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import { ManagementSetup } from '@kbn/management-plugin/public'; import { SharePluginStart } from '@kbn/share-plugin/public'; -import { ExtensionsSetup } from './services'; +import { ExtensionsSetup, PublicApiServiceSetup } from './services'; export interface IndexManagementPluginSetup { + apiService: PublicApiServiceSetup; extensionsService: ExtensionsSetup; } @@ -29,4 +30,5 @@ export interface ClientConfigType { enabled: boolean; }; enableIndexActions?: boolean; + enableLegacyTemplates?: boolean; } diff --git a/x-pack/plugins/index_management/server/config.ts b/x-pack/plugins/index_management/server/config.ts index c5d459486a8ef..f480c7747ca8d 100644 --- a/x-pack/plugins/index_management/server/config.ts +++ b/x-pack/plugins/index_management/server/config.ts @@ -30,6 +30,14 @@ const schemaLatest = schema.object( schema.boolean({ defaultValue: true }), schema.never() ), + enableLegacyTemplates: schema.conditional( + schema.contextRef('serverless'), + true, + // Legacy templates functionality is disabled in serverless; refer to the serverless.yml file as the source of truth + // We take this approach in order to have a central place (serverless.yml) for serverless config across Kibana + schema.boolean({ defaultValue: true }), + schema.never() + ), }, { defaultValue: undefined } ); @@ -38,6 +46,7 @@ const configLatest: PluginConfigDescriptor = { exposeToBrowser: { ui: true, enableIndexActions: true, + enableLegacyTemplates: true, }, schema: schemaLatest, deprecations: () => [], diff --git a/x-pack/plugins/index_management/server/lib/enrich_policies.test.ts b/x-pack/plugins/index_management/server/lib/enrich_policies.test.ts new file mode 100644 index 0000000000000..15517b2bfc20f --- /dev/null +++ b/x-pack/plugins/index_management/server/lib/enrich_policies.test.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { serializeEnrichmentPolicies } from './enrich_policies'; +import { createTestESEnrichPolicy } from '../test/helpers'; + +describe('serializeEnrichmentPolicies', () => { + it('knows how to serialize a list of policies', async () => { + const mockedESPolicy = createTestESEnrichPolicy('my-policy', 'match'); + expect(serializeEnrichmentPolicies([mockedESPolicy])).toEqual([ + { + name: 'my-policy', + type: 'match', + sourceIndices: ['users'], + matchField: 'email', + enrichFields: ['first_name', 'last_name', 'city'], + }, + ]); + }); +}); diff --git a/x-pack/plugins/index_management/server/lib/enrich_policies.ts b/x-pack/plugins/index_management/server/lib/enrich_policies.ts new file mode 100644 index 0000000000000..ca1748a380c70 --- /dev/null +++ b/x-pack/plugins/index_management/server/lib/enrich_policies.ts @@ -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 { IScopedClusterClient } from '@kbn/core/server'; +import type { EnrichSummary, EnrichPolicyType } from '@elastic/elasticsearch/lib/api/types'; +import type { SerializedEnrichPolicy } from '../../common/types'; + +const getPolicyType = (policy: EnrichSummary): EnrichPolicyType => { + if (policy.config.match) { + return 'match'; + } + + if (policy.config.geo_match) { + return 'geo_match'; + } + + if (policy.config.range) { + return 'range'; + } + + throw new Error('Unknown policy type'); +}; + +export const serializeEnrichmentPolicies = ( + policies: EnrichSummary[] +): SerializedEnrichPolicy[] => { + return policies.map((policy: any) => { + const policyType = getPolicyType(policy); + + return { + name: policy.config[policyType].name, + type: policyType, + sourceIndices: policy.config[policyType].indices, + matchField: policy.config[policyType].match_field, + enrichFields: policy.config[policyType].enrich_fields, + }; + }); +}; + +const fetchAll = async (client: IScopedClusterClient) => { + const res = await client.asCurrentUser.enrich.getPolicy(); + + return serializeEnrichmentPolicies(res.policies); +}; + +export const enrichPoliciesActions = { + fetchAll, +}; diff --git a/x-pack/plugins/index_management/server/plugin.ts b/x-pack/plugins/index_management/server/plugin.ts index a36101ad2911e..a42216d9f1bb7 100644 --- a/x-pack/plugins/index_management/server/plugin.ts +++ b/x-pack/plugins/index_management/server/plugin.ts @@ -12,6 +12,7 @@ import { Dependencies } from './types'; import { ApiRoutes } from './routes'; import { IndexDataEnricher } from './services'; import { handleEsError } from './shared_imports'; +import { IndexManagementConfig } from './config'; export interface IndexManagementPluginSetup { indexDataEnricher: { @@ -22,10 +23,12 @@ export interface IndexManagementPluginSetup { export class IndexMgmtServerPlugin implements Plugin { private readonly apiRoutes: ApiRoutes; private readonly indexDataEnricher: IndexDataEnricher; + private readonly config: IndexManagementConfig; constructor(initContext: PluginInitializerContext) { this.apiRoutes = new ApiRoutes(); this.indexDataEnricher = new IndexDataEnricher(); + this.config = initContext.config.get(); } setup( @@ -51,6 +54,7 @@ export class IndexMgmtServerPlugin implements Plugin security !== undefined && security.license.isEnabled(), + isLegacyTemplatesEnabled: this.config.enableLegacyTemplates, }, indexDataEnricher: this.indexDataEnricher, lib: { diff --git a/x-pack/plugins/index_management/server/routes/api/component_templates/register_privileges_route.test.ts b/x-pack/plugins/index_management/server/routes/api/component_templates/register_privileges_route.test.ts index dc4214ae43f73..601695c64e054 100644 --- a/x-pack/plugins/index_management/server/routes/api/component_templates/register_privileges_route.test.ts +++ b/x-pack/plugins/index_management/server/routes/api/component_templates/register_privileges_route.test.ts @@ -46,6 +46,7 @@ describe('GET privileges', () => { router, config: { isSecurityEnabled: () => true, + isLegacyTemplatesEnabled: true, }, indexDataEnricher: mockedIndexDataEnricher, lib: { @@ -112,6 +113,7 @@ describe('GET privileges', () => { router, config: { isSecurityEnabled: () => false, + isLegacyTemplatesEnabled: true, }, indexDataEnricher: mockedIndexDataEnricher, lib: { diff --git a/x-pack/plugins/index_management/server/routes/api/enrich_policies/enrich_policies.test.ts b/x-pack/plugins/index_management/server/routes/api/enrich_policies/enrich_policies.test.ts new file mode 100644 index 0000000000000..57d8f3f05a3d6 --- /dev/null +++ b/x-pack/plugins/index_management/server/routes/api/enrich_policies/enrich_policies.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { addInternalBasePath } from '..'; +import { RouterMock, routeDependencies, RequestMock } from '../../../test/helpers'; +import { serializeEnrichmentPolicies } from '../../../lib/enrich_policies'; +import { createTestESEnrichPolicy } from '../../../test/helpers'; + +import { registerEnrichPoliciesRoute } from './register_enrich_policies_routes'; + +const mockedPolicy = createTestESEnrichPolicy('my-policy', 'match'); + +describe('Enrich policies API', () => { + const router = new RouterMock(); + + beforeEach(() => { + registerEnrichPoliciesRoute({ + ...routeDependencies, + router, + }); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('Get all policies - GET /internal/index_management/enrich_policies', () => { + const getEnrichPolicies = router.getMockESApiFn('enrich.getPolicy'); + + it('returns all available policies', async () => { + const mockRequest: RequestMock = { + method: 'get', + path: addInternalBasePath('/enrich_policies'), + }; + + getEnrichPolicies.mockResolvedValue({ policies: [mockedPolicy] }); + + const res = await router.runRequest(mockRequest); + + expect(res).toEqual({ + body: serializeEnrichmentPolicies([mockedPolicy]), + }); + }); + + it('should return an error if it fails', async () => { + const mockRequest: RequestMock = { + method: 'get', + path: addInternalBasePath('/enrich_policies'), + }; + + const error = new Error('Oh no!'); + getEnrichPolicies.mockRejectedValue(error); + + await expect(router.runRequest(mockRequest)).rejects.toThrowError(error); + }); + }); +}); diff --git a/x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_title/index.ts b/x-pack/plugins/index_management/server/routes/api/enrich_policies/index.ts similarity index 75% rename from x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_title/index.ts rename to x-pack/plugins/index_management/server/routes/api/enrich_policies/index.ts index 1cac5db9302b7..945728dfff9d8 100644 --- a/x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_title/index.ts +++ b/x-pack/plugins/index_management/server/routes/api/enrich_policies/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { AdvancedSettingsTitle } from './advanced_settings_title'; +export { registerEnrichPoliciesRoute } from './register_enrich_policies_routes'; diff --git a/x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_subtitle/index.ts b/x-pack/plugins/index_management/server/routes/api/enrich_policies/register_enrich_policies_routes.ts similarity index 52% rename from x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_subtitle/index.ts rename to x-pack/plugins/index_management/server/routes/api/enrich_policies/register_enrich_policies_routes.ts index b8dccf656b8ee..ccafe26a2e68f 100644 --- a/x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_subtitle/index.ts +++ b/x-pack/plugins/index_management/server/routes/api/enrich_policies/register_enrich_policies_routes.ts @@ -5,4 +5,10 @@ * 2.0. */ -export { AdvancedSettingsSubtitle } from './advanced_settings_subtitle'; +import { RouteDependencies } from '../../../types'; + +import { registerListRoute } from './register_list_route'; + +export function registerEnrichPoliciesRoute(dependencies: RouteDependencies) { + registerListRoute(dependencies); +} diff --git a/x-pack/plugins/index_management/server/routes/api/enrich_policies/register_list_route.ts b/x-pack/plugins/index_management/server/routes/api/enrich_policies/register_list_route.ts new file mode 100644 index 0000000000000..1df52d8f2ba17 --- /dev/null +++ b/x-pack/plugins/index_management/server/routes/api/enrich_policies/register_list_route.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IScopedClusterClient } from '@kbn/core/server'; +import { RouteDependencies } from '../../../types'; +import { addInternalBasePath } from '..'; +import { enrichPoliciesActions } from '../../../lib/enrich_policies'; + +export function registerListRoute({ router, lib: { handleEsError } }: RouteDependencies) { + router.get( + { path: addInternalBasePath('/enrich_policies'), validate: false }, + async (context, request, response) => { + const client = (await context.core).elasticsearch.client as IScopedClusterClient; + try { + const policies = await enrichPoliciesActions.fetchAll(client); + return response.ok({ body: policies }); + } catch (error) { + return handleEsError({ error, response }); + } + } + ); +} diff --git a/x-pack/plugins/index_management/server/routes/api/index.ts b/x-pack/plugins/index_management/server/routes/api/index.ts index 98b16e21913a7..85d937717f41f 100644 --- a/x-pack/plugins/index_management/server/routes/api/index.ts +++ b/x-pack/plugins/index_management/server/routes/api/index.ts @@ -5,6 +5,8 @@ * 2.0. */ -import { API_BASE_PATH } from '../../../common'; +import { API_BASE_PATH, INTERNAL_API_BASE_PATH } from '../../../common'; export const addBasePath = (uri: string): string => API_BASE_PATH + uri; + +export const addInternalBasePath = (uri: string): string => INTERNAL_API_BASE_PATH + uri; diff --git a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts index 32661bb308876..ce389af9b13e8 100644 --- a/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts +++ b/x-pack/plugins/index_management/server/routes/api/templates/register_get_routes.ts @@ -17,7 +17,7 @@ import { getCloudManagedTemplatePrefix } from '../../../lib/get_managed_template import { RouteDependencies } from '../../../types'; import { addBasePath } from '..'; -export function registerGetAllRoute({ router, lib: { handleEsError } }: RouteDependencies) { +export function registerGetAllRoute({ router, config, lib: { handleEsError } }: RouteDependencies) { router.get( { path: addBasePath('/index_templates'), validate: false }, async (context, request, response) => { @@ -25,17 +25,24 @@ export function registerGetAllRoute({ router, lib: { handleEsError } }: RouteDep try { const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(client); - - const legacyTemplatesEs = await client.asCurrentUser.indices.getTemplate(); const { index_templates: templatesEs } = await client.asCurrentUser.indices.getIndexTemplate(); + // @ts-expect-error TemplateSerialized.index_patterns not compatible with IndicesIndexTemplate.index_patterns + const templates = deserializeTemplateList(templatesEs, cloudManagedTemplatePrefix); + + if (config.isLegacyTemplatesEnabled === false) { + // If isLegacyTemplatesEnabled=false, we do not want to fetch legacy templates and return an empty array; + // we retain the same response format to limit changes required on the client + return response.ok({ body: { templates, legacyTemplates: [] } }); + } + + const legacyTemplatesEs = await client.asCurrentUser.indices.getTemplate(); + const legacyTemplates = deserializeLegacyTemplateList( legacyTemplatesEs, cloudManagedTemplatePrefix ); - // @ts-expect-error TemplateSerialized.index_patterns not compatible with IndicesIndexTemplate.index_patterns - const templates = deserializeTemplateList(templatesEs, cloudManagedTemplatePrefix); const body = { templates, @@ -59,7 +66,7 @@ const querySchema = schema.object({ legacy: schema.maybe(schema.oneOf([schema.literal('true'), schema.literal('false')])), }); -export function registerGetOneRoute({ router, lib: { handleEsError } }: RouteDependencies) { +export function registerGetOneRoute({ router, config, lib: { handleEsError } }: RouteDependencies) { router.get( { path: addBasePath('/index_templates/{name}'), @@ -68,7 +75,10 @@ export function registerGetOneRoute({ router, lib: { handleEsError } }: RouteDep async (context, request, response) => { const { client } = (await context.core).elasticsearch; const { name } = request.params as TypeOf; - const isLegacy = (request.query as TypeOf).legacy === 'true'; + // We don't expect the `legacy` query to be used when legacy templates are disabled, however, we add the `enableLegacyTemplates` check as a safeguard + const isLegacy = + config.isLegacyTemplatesEnabled !== false && + (request.query as TypeOf).legacy === 'true'; try { const cloudManagedTemplatePrefix = await getCloudManagedTemplatePrefix(client); diff --git a/x-pack/plugins/index_management/server/routes/index.ts b/x-pack/plugins/index_management/server/routes/index.ts index e2a2eaf1184f6..79d90762920bf 100644 --- a/x-pack/plugins/index_management/server/routes/index.ts +++ b/x-pack/plugins/index_management/server/routes/index.ts @@ -15,6 +15,7 @@ import { registerSettingsRoutes } from './api/settings'; import { registerStatsRoute } from './api/stats'; import { registerComponentTemplateRoutes } from './api/component_templates'; import { registerNodesRoute } from './api/nodes'; +import { registerEnrichPoliciesRoute } from './api/enrich_policies'; export class ApiRoutes { setup(dependencies: RouteDependencies) { @@ -26,6 +27,7 @@ export class ApiRoutes { registerMappingRoute(dependencies); registerComponentTemplateRoutes(dependencies); registerNodesRoute(dependencies); + registerEnrichPoliciesRoute(dependencies); } start() {} diff --git a/x-pack/plugins/index_management/server/test/helpers/index.ts b/x-pack/plugins/index_management/server/test/helpers/index.ts index 682b520c12b00..cfce28a430198 100644 --- a/x-pack/plugins/index_management/server/test/helpers/index.ts +++ b/x-pack/plugins/index_management/server/test/helpers/index.ts @@ -9,3 +9,5 @@ export type { RequestMock } from './router_mock'; export { RouterMock } from './router_mock'; export { routeDependencies } from './route_dependencies'; + +export { createTestESEnrichPolicy } from './policies_fixtures'; diff --git a/x-pack/plugins/index_management/server/test/helpers/policies_fixtures.ts b/x-pack/plugins/index_management/server/test/helpers/policies_fixtures.ts new file mode 100644 index 0000000000000..235c4ad80a141 --- /dev/null +++ b/x-pack/plugins/index_management/server/test/helpers/policies_fixtures.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { EnrichPolicyType } from '@elastic/elasticsearch/lib/api/types'; + +export const createTestESEnrichPolicy = (name: string, type: EnrichPolicyType) => ({ + config: { + [type]: { + name, + indices: ['users'], + match_field: 'email', + enrich_fields: ['first_name', 'last_name', 'city'], + }, + }, +}); diff --git a/x-pack/plugins/index_management/server/test/helpers/route_dependencies.ts b/x-pack/plugins/index_management/server/test/helpers/route_dependencies.ts index 592e7490cdbe2..bfcf2a18a7736 100644 --- a/x-pack/plugins/index_management/server/test/helpers/route_dependencies.ts +++ b/x-pack/plugins/index_management/server/test/helpers/route_dependencies.ts @@ -12,6 +12,7 @@ import type { RouteDependencies } from '../../types'; export const routeDependencies: Omit = { config: { isSecurityEnabled: jest.fn().mockReturnValue(true), + isLegacyTemplatesEnabled: true, }, indexDataEnricher: new IndexDataEnricher(), lib: { diff --git a/x-pack/plugins/index_management/server/types.ts b/x-pack/plugins/index_management/server/types.ts index fc245fb664f9c..bd3d889f2bce9 100644 --- a/x-pack/plugins/index_management/server/types.ts +++ b/x-pack/plugins/index_management/server/types.ts @@ -23,6 +23,7 @@ export interface RouteDependencies { router: IRouter; config: { isSecurityEnabled: () => boolean; + isLegacyTemplatesEnabled: boolean; }; indexDataEnricher: IndexDataEnricher; lib: { diff --git a/x-pack/plugins/observability/common/index.ts b/x-pack/plugins/observability/common/index.ts index 9459802d820db..a90240d95e054 100644 --- a/x-pack/plugins/observability/common/index.ts +++ b/x-pack/plugins/observability/common/index.ts @@ -40,6 +40,7 @@ export { apmEnableContinuousRollups, enableCriticalPath, syntheticsThrottlingEnabled, + apmEnableProfilingIntegration, } from './ui_settings_keys'; export { diff --git a/x-pack/plugins/observability/common/ui_settings_keys.ts b/x-pack/plugins/observability/common/ui_settings_keys.ts index 833b6890dba03..183e9f41030ce 100644 --- a/x-pack/plugins/observability/common/ui_settings_keys.ts +++ b/x-pack/plugins/observability/common/ui_settings_keys.ts @@ -26,3 +26,4 @@ export const apmEnableServiceMetrics = 'observability:apmEnableServiceMetrics'; export const apmEnableContinuousRollups = 'observability:apmEnableContinuousRollups'; export const syntheticsThrottlingEnabled = 'observability:syntheticsThrottlingEnabled'; export const enableLegacyUptimeApp = 'observability:enableLegacyUptimeApp'; +export const apmEnableProfilingIntegration = 'observability:apmEnableProfilingIntegration'; diff --git a/x-pack/plugins/observability/server/ui_settings.ts b/x-pack/plugins/observability/server/ui_settings.ts index 637856d91c7e9..39678758bd7e1 100644 --- a/x-pack/plugins/observability/server/ui_settings.ts +++ b/x-pack/plugins/observability/server/ui_settings.ts @@ -29,6 +29,7 @@ import { enableInfrastructureHostsView, syntheticsThrottlingEnabled, enableLegacyUptimeApp, + apmEnableProfilingIntegration, } from '../common/ui_settings_keys'; const betaLabel = i18n.translate('xpack.observability.uiSettings.betaLabel', { @@ -364,6 +365,15 @@ export const uiSettings: Record = { schema: schema.boolean(), requiresPageReload: true, }, + [apmEnableProfilingIntegration]: { + category: [observabilityFeatureId], + name: i18n.translate('xpack.observability.apmEnableProfilingIntegration', { + defaultMessage: 'Enable Universal Profiling integration in APM', + }), + value: false, + schema: schema.boolean(), + requiresPageReload: false, + }, }; function throttlingDocsLink({ href }: { href: string }) { diff --git a/x-pack/plugins/observability_onboarding/public/components/app/system_logs/install_elastic_agent.tsx b/x-pack/plugins/observability_onboarding/public/components/app/system_logs/install_elastic_agent.tsx index 5f12dead46f23..470ce4517bed4 100644 --- a/x-pack/plugins/observability_onboarding/public/components/app/system_logs/install_elastic_agent.tsx +++ b/x-pack/plugins/observability_onboarding/public/components/app/system_logs/install_elastic_agent.tsx @@ -37,6 +37,7 @@ import { } from '../../shared/step_panel'; import { ApiKeyBanner } from '../custom_logs/wizard/api_key_banner'; import { getDiscoverNavigationParams } from '../utils'; +import { SystemIntegrationBanner } from './system_integration_banner'; import { TroubleshootingLink } from '../../shared/troubleshooting_link'; export function InstallElasticAgent() { @@ -226,6 +227,8 @@ export function InstallElasticAgent() {

+ + {apiKeyEncoded && onboardingId ? ( (); + const [error, setError] = useState(); + + const onIntegrationCreationSuccess = useCallback( + ({ version }: { version?: string }) => { + setIntegrationVersion(version); + }, + [] + ); + + const onIntegrationCreationFailure = useCallback( + (e: SystemIntegrationError) => { + setError(e); + }, + [] + ); + + const { performRequest, requestState } = useInstallSystemIntegration({ + onIntegrationCreationSuccess, + onIntegrationCreationFailure, + }); + + useEffect(() => { + performRequest(); + }, [performRequest]); + + const isInstallingIntegration = requestState.state === 'pending'; + const hasFailedInstallingIntegration = requestState.state === 'rejected'; + const hasInstalledIntegration = requestState.state === 'resolved'; + + if (isInstallingIntegration) { + return ( + + + + + + {i18n.translate( + 'xpack.observability_onboarding.systemIntegration.installing', + { + defaultMessage: 'Installing system integration', + } + )} + + + } + color="primary" + /> + ); + } + if (hasFailedInstallingIntegration) { + return ( + + + {error?.message} + + + ); + } + if (hasInstalledIntegration) { + return ( + + + + + {i18n.translate( + 'xpack.observability_onboarding.systemIntegration.installed.tooltip.description', + { + defaultMessage: + 'Integrations streamline connecting your data to the Elastic Stack.', + } + )} + + + { + event.preventDefault(); + navigateToAppUrl( + `/integrations/detail/system-${integrationVersion}` + ); + }} + > + {i18n.translate( + 'xpack.observability_onboarding.systemIntegration.installed.tooltip.link.label', + { + defaultMessage: 'Learn more', + } + )} + + ), + }} + /> + + + + ), + }} + /> + } + color="success" + iconType="check" + /> + + ); + } + return null; +} diff --git a/x-pack/plugins/observability_onboarding/public/components/shared/popover_tooltip.tsx b/x-pack/plugins/observability_onboarding/public/components/shared/popover_tooltip.tsx new file mode 100644 index 0000000000000..66165edc8e133 --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/components/shared/popover_tooltip.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiButtonIcon, EuiPopover, EuiPopoverTitle } from '@elastic/eui'; +import React, { useState } from 'react'; + +interface PopoverTooltipProps { + ariaLabel?: string; + iconType?: string; + title?: string; + children: React.ReactNode; +} + +export function PopoverTooltip({ + ariaLabel, + iconType = 'iInCircle', + title, + children, +}: PopoverTooltipProps) { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + return ( + setIsPopoverOpen(false)} + style={{ margin: '-5px 0 0 -5px' }} + button={ + ) => { + setIsPopoverOpen(!isPopoverOpen); + event.stopPropagation(); + }} + size="xs" + color="primary" + iconType={iconType} + /> + } + > + {title && {title}} + {children} + + ); +} diff --git a/x-pack/plugins/observability_onboarding/public/hooks/use_install_system_integration.ts b/x-pack/plugins/observability_onboarding/public/hooks/use_install_system_integration.ts new file mode 100644 index 0000000000000..5473ebd09c240 --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/hooks/use_install_system_integration.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback } from 'react'; +import { useTrackedPromise } from '@kbn/use-tracked-promise'; +import { i18n } from '@kbn/i18n'; +import { useKibana } from './use_kibana'; + +// Errors +const UNAUTHORIZED_ERROR = i18n.translate( + 'xpack.observability_onboarding.installSystemIntegration.error.unauthorized', + { + defaultMessage: + 'Required kibana privilege {requiredKibanaPrivileges} is missing, please add the required privilege to the role of the authenticated user.', + values: { + requiredKibanaPrivileges: "['Fleet', 'Integrations']", + }, + } +); + +type ErrorType = 'AuthorizationError' | 'UnknownError'; +export interface SystemIntegrationError { + type: ErrorType; + message: string; +} + +type IntegrationInstallStatus = + | 'installed' + | 'installing' + | 'install_failed' + | 'not_installed'; + +export const useInstallSystemIntegration = ({ + onIntegrationCreationSuccess, + onIntegrationCreationFailure, +}: { + onIntegrationCreationSuccess: ({ version }: { version?: string }) => void; + onIntegrationCreationFailure: (error: SystemIntegrationError) => void; +}) => { + const { + services: { http }, + } = useKibana(); + const [requestState, callPerformRequest] = useTrackedPromise( + { + cancelPreviousOn: 'creation', + createPromise: async () => { + const { item: systemIntegration } = await http.get<{ + item: { version: string; status: IntegrationInstallStatus }; + }>('/api/fleet/epm/packages/system'); + + if (systemIntegration.status !== 'installed') { + await http.post('/api/fleet/epm/packages/system'); + } + + return { + version: systemIntegration.version, + }; + }, + onResolve: ({ version }: { version?: string }) => { + onIntegrationCreationSuccess({ version }); + }, + onReject: (requestError: any) => { + if (requestError?.body?.statusCode === 403) { + onIntegrationCreationFailure({ + type: 'AuthorizationError' as const, + message: UNAUTHORIZED_ERROR, + }); + } else { + onIntegrationCreationFailure({ + type: 'UnknownError' as const, + message: requestError?.body?.message, + }); + } + }, + }, + [onIntegrationCreationSuccess, onIntegrationCreationFailure] + ); + + const performRequest = useCallback(() => { + callPerformRequest(); + }, [callPerformRequest]); + + return { + performRequest, + requestState, + }; +}; diff --git a/x-pack/plugins/observability_onboarding/public/hooks/use_kibana.ts b/x-pack/plugins/observability_onboarding/public/hooks/use_kibana.ts new file mode 100644 index 0000000000000..3102d3903b85f --- /dev/null +++ b/x-pack/plugins/observability_onboarding/public/hooks/use_kibana.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CoreStart } from '@kbn/core/public'; +import { + context as KibanaContext, + KibanaContextProvider, + useKibana, +} from '@kbn/kibana-react-plugin/public'; + +export type Services = CoreStart; + +const useTypedKibana = () => useKibana(); + +export { KibanaContextProvider, useTypedKibana as useKibana, KibanaContext }; diff --git a/x-pack/plugins/profiling/public/views/no_data_view/index.tsx b/x-pack/plugins/profiling/public/views/no_data_view/index.tsx index e982e4d53e308..c50f50d3170ed 100644 --- a/x-pack/plugins/profiling/public/views/no_data_view/index.tsx +++ b/x-pack/plugins/profiling/public/views/no_data_view/index.tsx @@ -330,7 +330,7 @@ docker.elastic.co/observability/profiling-agent:${hostAgentVersion} /root/pf-hos iconType="gear" fill href={`${core.http.basePath.prepend( - `/app/integrations/detail/profiler_agent-${data?.profilerAgent.version}/overview?prerelease=true` + `/app/integrations/detail/profiler_agent-${data?.profilerAgent.version}/overview` )}`} > {i18n.translate('xpack.profiling.tabs.elasticAgentIntegrarion.step2.button', { diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/error_schema.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/model/error_schema.schema.yaml index 6953912ad1a18..7e9a11ccf56dc 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/error_schema.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/error_schema.schema.yaml @@ -4,6 +4,7 @@ info: version: 'not applicable' paths: {} components: + x-codegen-enabled: false schemas: ErrorSchema: type: object diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml index 6d9c48581578b..0e5a602e71018 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml @@ -4,6 +4,7 @@ info: version: 'not applicable' paths: {} components: + x-codegen-enabled: false schemas: UUID: type: string diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml index acffa91ca5b74..a4cdcae498e7a 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/rule_schemas.schema.yaml @@ -4,6 +4,7 @@ info: version: 'not applicable' paths: {} components: + x-codegen-enabled: false schemas: SortOrder: type: string @@ -31,7 +32,6 @@ components: type: object description: |- Rule execution result is an aggregate that groups plain rule execution events by execution UUID. - It contains such information as execution UUID, date, status and metrics. properties: execution_uuid: type: string diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/warning_schema.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/warning_schema.gen.ts new file mode 100644 index 0000000000000..9bb7d32c19645 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/warning_schema.gen.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { z } from 'zod'; + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator `yarn openapi:generate`. + */ + +export type WarningSchema = z.infer; +export const WarningSchema = z.object({ + type: z.string(), + message: z.string(), + actionPath: z.string(), + buttonLabel: z.string().optional(), +}); diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route.schema.yaml index 91e5c6b7150f4..deea1b32aa3aa 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/get_prebuilt_rules_and_timelines_status/get_prebuilt_rules_and_timelines_status_route.schema.yaml @@ -16,46 +16,41 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/GetPrebuiltRulesStatusResponse' - -components: - schemas: - GetPrebuiltRulesStatusResponse: - type: object - properties: - rules_custom_installed: - type: integer - description: The total number of custom rules - minimum: 0 - rules_installed: - type: integer - description: The total number of installed prebuilt rules - minimum: 0 - rules_not_installed: - type: integer - description: The total number of available prebuilt rules that are not installed - minimum: 0 - rules_not_updated: - type: integer - description: The total number of outdated prebuilt rules - minimum: 0 - timelines_installed: - type: integer - description: The total number of installed prebuilt timelines - minimum: 0 - timelines_not_installed: - type: integer - description: The total number of available prebuilt timelines that are not installed - minimum: 0 - timelines_not_updated: - type: integer - description: The total number of outdated prebuilt timelines - minimum: 0 - required: - - rules_custom_installed - - rules_installed - - rules_not_installed - - rules_not_updated - - timelines_installed - - timelines_not_installed - - timelines_not_updated + type: object + properties: + rules_custom_installed: + type: integer + description: The total number of custom rules + minimum: 0 + rules_installed: + type: integer + description: The total number of installed prebuilt rules + minimum: 0 + rules_not_installed: + type: integer + description: The total number of available prebuilt rules that are not installed + minimum: 0 + rules_not_updated: + type: integer + description: The total number of outdated prebuilt rules + minimum: 0 + timelines_installed: + type: integer + description: The total number of installed prebuilt timelines + minimum: 0 + timelines_not_installed: + type: integer + description: The total number of available prebuilt timelines that are not installed + minimum: 0 + timelines_not_updated: + type: integer + description: The total number of outdated prebuilt timelines + minimum: 0 + required: + - rules_custom_installed + - rules_installed + - rules_not_installed + - rules_not_updated + - timelines_installed + - timelines_not_installed + - timelines_not_updated diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.schema.yaml index ec3ce832e3ef4..a7c2309d4a542 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/detection_engine/prebuilt_rules/install_prebuilt_rules_and_timelines/install_prebuilt_rules_and_timelines_route.schema.yaml @@ -16,31 +16,26 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/InstallPrebuiltRulesResponse' - -components: - schemas: - InstallPrebuiltRulesResponse: - type: object - properties: - rules_installed: - type: integer - description: The number of rules installed - minimum: 0 - rules_updated: - type: integer - description: The number of rules updated - minimum: 0 - timelines_installed: - type: integer - description: The number of timelines installed - minimum: 0 - timelines_updated: - type: integer - description: The number of timelines updated - minimum: 0 - required: - - rules_installed - - rules_updated - - timelines_installed - - timelines_updated + type: object + properties: + rules_installed: + type: integer + description: The number of rules installed + minimum: 0 + rules_updated: + type: integer + description: The number of rules updated + minimum: 0 + timelines_installed: + type: integer + description: The number of timelines installed + minimum: 0 + timelines_updated: + type: integer + description: The number of timelines updated + minimum: 0 + required: + - rules_installed + - rules_updated + - timelines_installed + - timelines_updated diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.schema.yaml index f30f009e4b6a0..8eba09881bbd9 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_actions/bulk_actions_route.schema.yaml @@ -34,6 +34,7 @@ paths: $ref: '#/components/schemas/BulkEditActionResponse' components: + x-codegen-enabled: false schemas: BulkEditSkipReason: type: string diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_crud/bulk_update_rules/bulk_patch_rules_route.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_crud/bulk_update_rules/bulk_update_rules_route.schema.yaml similarity index 100% rename from x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_crud/bulk_update_rules/bulk_patch_rules_route.schema.yaml rename to x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_crud/bulk_update_rules/bulk_update_rules_route.schema.yaml diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_crud/response_schema.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_crud/response_schema.schema.yaml index 99781d15f8eaa..b30ac7135c64d 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_crud/response_schema.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/bulk_crud/response_schema.schema.yaml @@ -4,6 +4,7 @@ info: version: 8.9.0 paths: {} components: + x-codegen-enabled: false schemas: BulkCrudRulesResponse: type: array diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/coverage_overview/coverage_overview_route.mock.ts b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/coverage_overview/coverage_overview_route.mock.ts new file mode 100644 index 0000000000000..3c65ea53adf9a --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/detection_engine/rule_management/coverage_overview/coverage_overview_route.mock.ts @@ -0,0 +1,17 @@ +/* + * 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 { + CoverageOverviewRuleSource, + CoverageOverviewRuleActivity, +} from './coverage_overview_route'; + +export const getCoverageOverviewFilterMock = () => ({ + search_term: 'test query', + activity: [CoverageOverviewRuleActivity.Enabled], + source: [CoverageOverviewRuleSource.Prebuilt], +}); diff --git a/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts b/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts index f85587dc40d7d..986895e12b41b 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts @@ -16,7 +16,8 @@ export const policyFactory = ( cloud = false, licenseUid = '', clusterUuid = '', - clusterName = '' + clusterName = '', + serverless = false ): PolicyConfig => { return { meta: { @@ -25,6 +26,7 @@ export const policyFactory = ( cluster_uuid: clusterUuid, cluster_name: clusterName, cloud, + serverless, }, windows: { events: { diff --git a/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts b/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts index fe3fd8c2ebd6a..8be5c054fcfb0 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/policy_config_helpers.test.ts @@ -192,7 +192,14 @@ describe('Policy Config helpers', () => { // This constant makes sure that if the type `PolicyConfig` is ever modified, // the logic for disabling protections is also modified due to type check. export const eventsOnlyPolicy = (): PolicyConfig => ({ - meta: { license: '', cloud: false, license_uid: '', cluster_name: '', cluster_uuid: '' }, + meta: { + license: '', + cloud: false, + license_uid: '', + cluster_name: '', + cluster_uuid: '', + serverless: false, + }, windows: { events: { credential_access: true, diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index 5702f14f2a37a..b71495d6288f2 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -945,6 +945,8 @@ export interface PolicyConfig { license_uid: string; cluster_uuid: string; cluster_name: string; + serverless: boolean; + heartbeatinterval?: number; }; windows: { advanced?: { diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_response/prebuilt_rules/prebuilt_rules_install_update_workflows.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_response/prebuilt_rules/prebuilt_rules_install_update_workflows.cy.ts index 4957d6edc3371..f148e973300dd 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_response/prebuilt_rules/prebuilt_rules_install_update_workflows.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_response/prebuilt_rules/prebuilt_rules_install_update_workflows.cy.ts @@ -60,8 +60,7 @@ describe('Detection rules, Prebuilt Rules Installation and Update workflow', () }); it('should install package from Fleet in the background', () => { - /* Assert that the package in installed from Fleet by checking that - /* the installSource is "registry", as opposed to "bundle" */ + /* Assert that the package in installed from Fleet */ cy.wait('@installPackageBulk', { timeout: 60000, }).then(({ response: bulkResponse }) => { @@ -70,7 +69,6 @@ describe('Detection rules, Prebuilt Rules Installation and Update workflow', () const packages = bulkResponse?.body.items.map( ({ name, result }: BulkInstallPackageInfo) => ({ name, - installSource: result.installSource, }) ); @@ -86,17 +84,14 @@ describe('Detection rules, Prebuilt Rules Installation and Update workflow', () cy.wrap(response?.body) .should('have.property', 'items') .should('have.length.greaterThan', 0); - cy.wrap(response?.body) - .should('have.property', '_meta') - .should('have.property', 'install_source') - .should('eql', 'registry'); }); } else { // Normal flow, install via the Fleet bulk install API expect(packages.length).to.have.greaterThan(0); - expect(packages).to.deep.include.members([ - { name: 'security_detection_engine', installSource: 'registry' }, - ]); + // At least one of the packages installed should be the security_detection_engine package + expect(packages).to.satisfy((pckgs: BulkInstallPackageInfo[]) => + pckgs.some((pkg) => pkg.name === 'security_detection_engine') + ); } }); }); diff --git a/x-pack/plugins/security_solution/cypress/e2e/detection_response/rule_creation/custom_saved_query_rule.cy.ts b/x-pack/plugins/security_solution/cypress/e2e/detection_response/rule_creation/custom_saved_query_rule.cy.ts index 3c43bca292602..8cb8fc2ba7576 100644 --- a/x-pack/plugins/security_solution/cypress/e2e/detection_response/rule_creation/custom_saved_query_rule.cy.ts +++ b/x-pack/plugins/security_solution/cypress/e2e/detection_response/rule_creation/custom_saved_query_rule.cy.ts @@ -9,7 +9,6 @@ import { getNewRule, getSavedQueryRule } from '../../../objects/rule'; import { DEFINE_CONTINUE_BUTTON, - CUSTOM_QUERY_BAR, LOAD_QUERY_DYNAMICALLY_CHECKBOX, QUERY_BAR, } from '../../../screens/create_new_rule'; @@ -37,7 +36,7 @@ import { } from '../../../tasks/create_new_rule'; import { saveEditedRule } from '../../../tasks/edit_rule'; import { login, visit } from '../../../tasks/login'; -import { getDetails } from '../../../tasks/rule_details'; +import { assertDetailsNotExist, getDetails } from '../../../tasks/rule_details'; import { createRule } from '../../../tasks/api_calls/rules'; import { RULE_CREATION, SECURITY_DETECTIONS_RULES_URL } from '../../../urls/navigation'; @@ -110,12 +109,29 @@ describe('Custom saved_query rules', () => { cy.get(TOASTER).should('contain', FAILED_TO_LOAD_ERROR); }); - // TODO: this error depended on the schema validation running. Can we show the error - // based on the saved query failing to load instead of relying on the schema validation? - it.skip('Shows validation error on rule edit when saved query can not be loaded', function () { + it('Shows validation error on rule edit when saved query can not be loaded', function () { editFirstRule(); - cy.get(CUSTOM_QUERY_BAR).should('contain', FAILED_TO_LOAD_ERROR); + cy.get(TOASTER).should('contain', FAILED_TO_LOAD_ERROR); + }); + + it('Allows to update saved_query rule with non-existent query', () => { + editFirstRule(); + + cy.get(LOAD_QUERY_DYNAMICALLY_CHECKBOX).should('exist'); + + cy.intercept('PUT', '/api/detection_engine/rules').as('editedRule'); + saveEditedRule(); + + cy.wait('@editedRule').then(({ response }) => { + // updated rule type shouldn't change + cy.wrap(response?.body.type).should('equal', 'saved_query'); + }); + + cy.get(DEFINE_RULE_PANEL_PROGRESS).should('not.exist'); + + assertDetailsNotExist(SAVED_QUERY_NAME_DETAILS); + assertDetailsNotExist(SAVED_QUERY_DETAILS); }); }); diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index 19c02e030d391..7f7b3e6f746e2 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -25,6 +25,8 @@ "test:generate": "node scripts/endpoint/resolver_generator", "mappings:generate": "node scripts/mappings/mappings_generator", "mappings:load": "node scripts/mappings/mappings_loader", - "junit:transform": "node scripts/junit_transformer --pathPattern '../../../target/kibana-security-solution/cypress/results/*.xml' --rootDirectory ../../../ --reportName 'Security Solution Cypress' --writeInPlace" + "junit:transform": "node scripts/junit_transformer --pathPattern '../../../target/kibana-security-solution/cypress/results/*.xml' --rootDirectory ../../../ --reportName 'Security Solution Cypress' --writeInPlace", + "openapi:generate": "node scripts/openapi/generate", + "openapi:generate:debug": "node --inspect-brk scripts/openapi/generate" } } diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx index 822a90cb0af6e..f749f63b3c5e4 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_creation_ui/pages/rule_editing/index.tsx @@ -20,7 +20,6 @@ import { FormattedMessage } from '@kbn/i18n-react'; import type { FC } from 'react'; import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useParams } from 'react-router-dom'; -import { noop } from 'lodash'; import type { DataViewListItem } from '@kbn/data-views-plugin/common'; import { RulePreview } from '../../../../detections/components/rules/rule_preview'; @@ -163,7 +162,6 @@ const EditRulePageComponent: FC<{ rule: Rule }> = ({ rule }) => { const { isSavedQueryLoading, savedQuery } = useGetSavedQuery({ savedQueryId: rule?.saved_id, ruleType: rule?.type, - onError: noop, }); // Since in the edit step we start with an existing rule, we assume that diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/constants.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/constants.ts index 76322812ecf27..85c734bbc077b 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/constants.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/constants.ts @@ -6,6 +6,11 @@ */ import { euiPalettePositive } from '@elastic/eui'; +import { + CoverageOverviewRuleActivity, + CoverageOverviewRuleSource, +} from '../../../../../common/api/detection_engine'; +import * as i18n from './translations'; export const coverageOverviewPaletteColors = euiPalettePositive(5); @@ -13,6 +18,8 @@ export const coverageOverviewPanelWidth = 160; export const coverageOverviewLegendWidth = 380; +export const coverageOverviewFilterWidth = 300; + /** * Rules count -> color map * @@ -24,3 +31,31 @@ export const coverageOverviewCardColorThresholds = [ { threshold: 3, color: coverageOverviewPaletteColors[1] }, { threshold: 1, color: coverageOverviewPaletteColors[0] }, ]; + +export const ruleActivityFilterDefaultOptions = [ + { + label: CoverageOverviewRuleActivity.Enabled, + }, + { + label: CoverageOverviewRuleActivity.Disabled, + }, +]; + +export const ruleActivityFilterLabelMap: Record = { + [CoverageOverviewRuleActivity.Enabled]: i18n.CoverageOverviewEnabledRuleActivity, + [CoverageOverviewRuleActivity.Disabled]: i18n.CoverageOverviewDisabledRuleActivity, +}; + +export const ruleSourceFilterDefaultOptions = [ + { + label: CoverageOverviewRuleSource.Prebuilt, + }, + { + label: CoverageOverviewRuleSource.Custom, + }, +]; + +export const ruleSourceFilterLabelMap: Record = { + [CoverageOverviewRuleSource.Prebuilt]: i18n.CoverageOverviewElasticRuleSource, + [CoverageOverviewRuleSource.Custom]: i18n.CoverageOverviewCustomRuleSource, +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_page.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard.test.tsx similarity index 72% rename from x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_page.test.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard.test.tsx index 794a8ca09d1f5..a761153c1e8d0 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_page.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard.test.tsx @@ -11,32 +11,35 @@ import { useFetchCoverageOverviewQuery } from '../../../rule_management/api/hook import { getMockCoverageOverviewDashboard } from '../../../rule_management/model/coverage_overview/__mocks__'; import { TestProviders } from '../../../../common/mock'; -import { CoverageOverviewPage } from './coverage_overview_page'; +import { CoverageOverviewDashboard } from './coverage_overview_dashboard'; +import { CoverageOverviewDashboardContextProvider } from './coverage_overview_dashboard_context'; jest.mock('../../../../common/utils/route/spy_routes', () => ({ SpyRoute: () => null })); jest.mock('../../../rule_management/api/hooks/use_fetch_coverage_overview'); (useFetchCoverageOverviewQuery as jest.Mock).mockReturnValue({ data: getMockCoverageOverviewDashboard(), + isLoading: false, + refetch: jest.fn(), }); const renderCoverageOverviewDashboard = () => { return render( - + + + ); }; -describe('CoverageOverviewPage', () => { +describe('CoverageOverviewDashboard', () => { beforeEach(() => { jest.clearAllMocks(); }); test('it renders', () => { - const wrapper = renderCoverageOverviewDashboard(); - - expect(wrapper.getByTestId('coverageOverviewPage')).toBeInTheDocument(); + renderCoverageOverviewDashboard(); expect(useFetchCoverageOverviewQuery).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard.tsx new file mode 100644 index 0000000000000..b9ed3be8ad6ac --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard.tsx @@ -0,0 +1,45 @@ +/* + * 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, EuiSpacer } from '@elastic/eui'; +import { HeaderPage } from '../../../../common/components/header_page'; + +import * as i18n from './translations'; +import { CoverageOverviewTacticPanel } from './tactic_panel'; +import { CoverageOverviewMitreTechniquePanelPopover } from './technique_panel_popover'; +import { CoverageOverviewFiltersPanel } from './filters_panel'; +import { useCoverageOverviewDashboardContext } from './coverage_overview_dashboard_context'; + +const CoverageOverviewDashboardComponent = () => { + const { + state: { data }, + } = useCoverageOverviewDashboardContext(); + return ( + <> + + + + + {data?.mitreTactics.map((tactic) => ( + + + + + + {tactic.techniques.map((technique, techniqueKey) => ( + + + + ))} + + ))} + + + ); +}; + +export const CoverageOverviewDashboard = CoverageOverviewDashboardComponent; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard_context.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard_context.tsx new file mode 100644 index 0000000000000..db96f1a5b8018 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard_context.tsx @@ -0,0 +1,160 @@ +/* + * 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, + useCallback, + useContext, + useEffect, + useMemo, + useReducer, +} from 'react'; +import { invariant } from '../../../../../common/utils/invariant'; +import type { + CoverageOverviewRuleActivity, + CoverageOverviewRuleSource, +} from '../../../../../common/api/detection_engine'; +import { BulkActionType } from '../../../../../common/api/detection_engine'; +import type { CoverageOverviewDashboardState } from './coverage_overview_dashboard_reducer'; +import { + SET_SHOW_EXPANDED_CELLS, + SET_RULE_ACTIVITY_FILTER, + SET_RULE_SOURCE_FILTER, + SET_RULE_SEARCH_FILTER, + createCoverageOverviewDashboardReducer, +} from './coverage_overview_dashboard_reducer'; +import { useFetchCoverageOverviewQuery } from '../../../rule_management/api/hooks/use_fetch_coverage_overview'; +import { useExecuteBulkAction } from '../../../rule_management/logic/bulk_actions/use_execute_bulk_action'; + +export interface CoverageOverviewDashboardActions { + refetch: () => void; + setShowExpandedCells: (value: boolean) => void; + setRuleActivityFilter: (value: CoverageOverviewRuleActivity[]) => void; + setRuleSourceFilter: (value: CoverageOverviewRuleSource[]) => void; + setRuleSearchFilter: (value: string) => void; + enableAllDisabled: (ruleIds: string[]) => Promise; +} + +export interface CoverageOverviewDashboardContextType { + state: CoverageOverviewDashboardState; + actions: CoverageOverviewDashboardActions; +} + +export const CoverageOverviewDashboardContext = + createContext(null); + +interface CoverageOverviewDashboardContextProviderProps { + children: React.ReactNode; +} + +export const initialState: CoverageOverviewDashboardState = { + showExpandedCells: false, + filter: {}, + data: undefined, + isLoading: false, +}; + +export const CoverageOverviewDashboardContextProvider = ({ + children, +}: CoverageOverviewDashboardContextProviderProps) => { + const [state, dispatch] = useReducer(createCoverageOverviewDashboardReducer(), initialState); + const { data, isLoading, refetch } = useFetchCoverageOverviewQuery(state.filter); + const { executeBulkAction } = useExecuteBulkAction(); + + useEffect(() => { + refetch(); + }, [refetch, state.filter]); + + const setShowExpandedCells = useCallback( + (value: boolean): void => { + dispatch({ + type: SET_SHOW_EXPANDED_CELLS, + value, + }); + }, + [dispatch] + ); + + const setRuleActivityFilter = useCallback( + (value: CoverageOverviewRuleActivity[]): void => { + dispatch({ + type: SET_RULE_ACTIVITY_FILTER, + value, + }); + }, + [dispatch] + ); + + const setRuleSourceFilter = useCallback( + (value: CoverageOverviewRuleSource[]): void => { + dispatch({ + type: SET_RULE_SOURCE_FILTER, + value, + }); + }, + [dispatch] + ); + + const setRuleSearchFilter = useCallback( + (value: string): void => { + dispatch({ + type: SET_RULE_SEARCH_FILTER, + value, + }); + }, + [dispatch] + ); + + const enableAllDisabled = useCallback( + async (ruleIds: string[]) => { + await executeBulkAction({ type: BulkActionType.enable, ids: ruleIds }); + }, + [executeBulkAction] + ); + + const actions = useMemo( + () => ({ + refetch, + setShowExpandedCells, + setRuleActivityFilter, + setRuleSourceFilter, + setRuleSearchFilter, + enableAllDisabled, + }), + [ + refetch, + setRuleActivityFilter, + setRuleSearchFilter, + setRuleSourceFilter, + setShowExpandedCells, + enableAllDisabled, + ] + ); + + const providerValue = useMemo(() => { + return { + state: { ...state, isLoading, data }, + actions, + }; + }, [actions, data, isLoading, state]); + + return ( + + {children} + + ); +}; + +export const useCoverageOverviewDashboardContext = (): CoverageOverviewDashboardContextType => { + const dashboardContext = useContext(CoverageOverviewDashboardContext); + invariant( + dashboardContext, + 'useCoverageOverviewDashboardContext should be used inside CoverageOverviewDashboardContextProvider' + ); + + return dashboardContext; +}; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard_reducer.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard_reducer.ts new file mode 100644 index 0000000000000..f62835f625647 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_dashboard_reducer.ts @@ -0,0 +1,75 @@ +/* + * 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 { + CoverageOverviewFilter, + CoverageOverviewRuleActivity, + CoverageOverviewRuleSource, +} from '../../../../../common/api/detection_engine'; +import type { CoverageOverviewDashboard } from '../../../rule_management/model/coverage_overview/dashboard'; + +export interface CoverageOverviewDashboardState { + showExpandedCells: boolean; + filter: CoverageOverviewFilter; + isLoading: boolean; + data: CoverageOverviewDashboard | undefined; +} + +// Action type names +export const SET_SHOW_EXPANDED_CELLS = 'setShowExpandedCells' as const; +export const SET_RULE_ACTIVITY_FILTER = 'setRuleActivityFilter' as const; +export const SET_RULE_SOURCE_FILTER = 'setRuleSourceFilter' as const; +export const SET_RULE_SEARCH_FILTER = 'setRuleSearchFilter' as const; + +export type Action = + | { + type: typeof SET_SHOW_EXPANDED_CELLS; + value: boolean; + } + | { + type: typeof SET_RULE_ACTIVITY_FILTER; + value: CoverageOverviewRuleActivity[]; + } + | { + type: typeof SET_RULE_SOURCE_FILTER; + value: CoverageOverviewRuleSource[]; + } + | { + type: typeof SET_RULE_SEARCH_FILTER; + value: string; + }; + +export const createCoverageOverviewDashboardReducer = + () => + (state: CoverageOverviewDashboardState, action: Action): CoverageOverviewDashboardState => { + switch (action.type) { + case SET_SHOW_EXPANDED_CELLS: { + const { value } = action; + return { ...state, showExpandedCells: value }; + } + case SET_RULE_ACTIVITY_FILTER: { + const { value } = action; + const updatedFilter = { ...state.filter, activity: value.length !== 0 ? value : undefined }; + return { ...state, filter: updatedFilter }; + } + case SET_RULE_SOURCE_FILTER: { + const { value } = action; + const updatedFilter = { ...state.filter, source: value.length !== 0 ? value : undefined }; + return { ...state, filter: updatedFilter }; + } + case SET_RULE_SEARCH_FILTER: { + const { value } = action; + const updatedFilter = { + ...state.filter, + search_term: value.length !== 0 ? value : undefined, + }; + return { ...state, filter: updatedFilter }; + } + default: + return state; + } + }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_page.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_page.tsx deleted file mode 100644 index ae2115d031e50..0000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/coverage_overview_page.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React, { useCallback, useReducer } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { SecuritySolutionPageWrapper } from '../../../../common/components/page_wrapper'; -import { SpyRoute } from '../../../../common/utils/route/spy_routes'; -import { SecurityPageName } from '../../../../app/types'; -import { HeaderPage } from '../../../../common/components/header_page'; - -import * as i18n from './translations'; -import { useFetchCoverageOverviewQuery } from '../../../rule_management/api/hooks/use_fetch_coverage_overview'; -import { CoverageOverviewTacticPanel } from './tactic_panel'; -import { CoverageOverviewMitreTechniquePanelPopover } from './technique_panel_popover'; -import { CoverageOverviewFiltersPanel } from './filters_panel'; -import { createCoverageOverviewDashboardReducer, initialState } from './reducer'; - -const CoverageOverviewPageComponent = () => { - const { data } = useFetchCoverageOverviewQuery(); - - const [{ showExpandedCells }, dispatch] = useReducer( - createCoverageOverviewDashboardReducer(), - initialState - ); - - const setShowExpandedCells = useCallback( - (value: boolean): void => { - dispatch({ - type: 'setShowExpandedCells', - value, - }); - }, - [dispatch] - ); - - return ( - <> - - - - - - - - {data?.mitreTactics.map((tactic) => ( - - - - - - {tactic.techniques.map((technique, techniqueKey) => ( - - - - ))} - - ))} - - - - ); -}; - -export const CoverageOverviewPage = React.memo(CoverageOverviewPageComponent); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/filter_panel.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/filter_panel.test.tsx new file mode 100644 index 0000000000000..6931298b7ed48 --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/filter_panel.test.tsx @@ -0,0 +1,86 @@ +/* + * 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 { fireEvent, render, within } from '@testing-library/react'; +import React from 'react'; + +import { TestProviders } from '../../../../common/mock'; +import { CoverageOverviewFiltersPanel } from './filters_panel'; +import { + ruleActivityFilterDefaultOptions, + ruleActivityFilterLabelMap, + ruleSourceFilterDefaultOptions, + ruleSourceFilterLabelMap, +} from './constants'; +import { + initialState, + useCoverageOverviewDashboardContext, +} from './coverage_overview_dashboard_context'; + +jest.mock('./coverage_overview_dashboard_context'); + +const setShowExpandedCells = jest.fn(); +const setRuleActivityFilter = jest.fn(); +const setRuleSourceFilter = jest.fn(); +const setRuleSearchFilter = jest.fn(); + +const mockCoverageOverviewContextReturn = { + state: initialState, + actions: { + setShowExpandedCells, + setRuleActivityFilter, + setRuleSourceFilter, + setRuleSearchFilter, + }, +}; + +(useCoverageOverviewDashboardContext as jest.Mock).mockReturnValue( + mockCoverageOverviewContextReturn +); + +const renderFiltersPanel = () => { + return render( + + + + ); +}; + +describe('CoverageOverviewFiltersPanel', () => { + test('it correctly populates rule activity filter state', () => { + const wrapper = renderFiltersPanel(); + + wrapper.getByTestId('coverageOverviewRuleActivityFilterButton').click(); + + within(wrapper.getByTestId('coverageOverviewFilterList')) + .getByText(ruleActivityFilterLabelMap[ruleActivityFilterDefaultOptions[0].label]) + .click(); + expect(setRuleActivityFilter).toHaveBeenCalledWith([ruleActivityFilterDefaultOptions[0].label]); + }); + + test('it correctly populates rule source filter state', () => { + const wrapper = renderFiltersPanel(); + + wrapper.getByTestId('coverageOverviewRuleSourceFilterButton').click(); + + within(wrapper.getByTestId('coverageOverviewFilterList')) + .getByText(ruleSourceFilterLabelMap[ruleSourceFilterDefaultOptions[0].label]) + .click(); + expect(setRuleSourceFilter).toHaveBeenCalledWith([ruleSourceFilterDefaultOptions[0].label]); + }); + + test('it correctly populates search filter state', () => { + const wrapper = renderFiltersPanel(); + + fireEvent.change(wrapper.getByTestId('coverageOverviewFilterSearchBar'), { + target: { value: 'test' }, + }); + fireEvent.submit(wrapper.getByTestId('coverageOverviewFilterSearchBar')); + + expect(setRuleSearchFilter).toHaveBeenCalledWith('test'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/filters_panel.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/filters_panel.tsx index 283f7a77d7036..e234b02257e19 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/filters_panel.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/filters_panel.tsx @@ -5,42 +5,91 @@ * 2.0. */ -import { EuiFilterButton, EuiFilterGroup, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui'; -import React, { memo } from 'react'; +import { + EuiFilterButton, + EuiFilterGroup, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiSearchBar, +} from '@elastic/eui'; +import React, { memo, useCallback } from 'react'; +import { css } from '@emotion/css'; import { CoverageOverviewLegend } from './shared_components/dashboard_legend'; import * as i18n from './translations'; +import { useCoverageOverviewDashboardContext } from './coverage_overview_dashboard_context'; +import { RuleActivityFilter } from './rule_activity_filter'; +import { RuleSourceFilter } from './rule_source_filter'; -export interface CoverageOverviewFiltersPanelProps { - setShowExpandedCells: (arg: boolean) => void; - showExpandedCells: boolean; -} +const CoverageOverviewFiltersPanelComponent = () => { + const { + state: { filter, isLoading, showExpandedCells }, + actions: { + setShowExpandedCells, + setRuleActivityFilter, + setRuleSourceFilter, + setRuleSearchFilter, + }, + } = useCoverageOverviewDashboardContext(); -const CoverageOverviewFiltersPanelComponent = ({ - setShowExpandedCells, - showExpandedCells, -}: CoverageOverviewFiltersPanelProps) => { const handleExpandCellsFilterClick = () => setShowExpandedCells(true); const handleCollapseCellsFilterClick = () => setShowExpandedCells(false); + const handleRuleSearchOnChange = useCallback( + ({ queryText }: { queryText: string }) => { + setRuleSearchFilter(queryText); + }, + [setRuleSearchFilter] + ); + return ( - - - - {i18n.COLLAPSE_CELLS_FILTER_BUTTON} - - - {i18n.EXPAND_CELLS_FILTER_BUTTON} - - + + + + + + + + + + + + + {i18n.COLLAPSE_CELLS_FILTER_BUTTON} + + + {i18n.EXPAND_CELLS_FILTER_BUTTON} + + + + diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/helpers.test.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/helpers.test.ts index b6d6c48749a10..5a1aee424352a 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/helpers.test.ts @@ -5,15 +5,23 @@ * 2.0. */ +import type { CoverageOverviewRuleActivity } from '../../../../../common/api/detection_engine'; +import { getCoverageOverviewFilterMock } from '../../../../../common/api/detection_engine/rule_management/coverage_overview/coverage_overview_route.mock'; import { getMockCoverageOverviewMitreSubTechnique, getMockCoverageOverviewMitreTactic, getMockCoverageOverviewMitreTechnique, } from '../../../rule_management/model/coverage_overview/__mocks__'; -import { getNumOfCoveredSubtechniques, getNumOfCoveredTechniques } from './helpers'; +import { ruleActivityFilterDefaultOptions } from './constants'; +import { + extractSelected, + getNumOfCoveredSubtechniques, + getNumOfCoveredTechniques, + populateSelected, +} from './helpers'; describe('helpers', () => { - describe('getCoveredTechniques', () => { + describe('getNumOfCoveredTechniques', () => { it('returns 0 when no techniques are present', () => { const payload = getMockCoverageOverviewMitreTactic(); expect(getNumOfCoveredTechniques(payload)).toEqual(0); @@ -31,7 +39,7 @@ describe('helpers', () => { }); }); - describe('getCoveredSubtechniques', () => { + describe('getNumOfCoveredSubtechniques', () => { it('returns 0 when no subtechniques are present', () => { const payload = getMockCoverageOverviewMitreTechnique(); expect(getNumOfCoveredSubtechniques(payload)).toEqual(0); @@ -48,4 +56,36 @@ describe('helpers', () => { expect(getNumOfCoveredSubtechniques(payload)).toEqual(2); }); }); + + describe('extractSelected', () => { + it('returns empty array when no options are checked', () => { + const payload = ruleActivityFilterDefaultOptions; + expect(extractSelected(payload)).toEqual([]); + }); + + it('returns checked options when present', () => { + const payload = [ + ...ruleActivityFilterDefaultOptions, + { ...ruleActivityFilterDefaultOptions[0], checked: 'on' }, + ]; + expect(extractSelected(payload)).toEqual([ruleActivityFilterDefaultOptions[0].label]); + }); + }); + + describe('populateSelected', () => { + it('returns default status options when no filter is present', () => { + const payload: CoverageOverviewRuleActivity[] = []; + expect(populateSelected(ruleActivityFilterDefaultOptions, payload)).toEqual( + ruleActivityFilterDefaultOptions + ); + }); + + it('returns correct options checked when present in filter', () => { + const payload = getCoverageOverviewFilterMock().activity; + expect(populateSelected(ruleActivityFilterDefaultOptions, payload)).toEqual([ + { label: 'enabled', checked: 'on' }, + { label: 'disabled' }, + ]); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/helpers.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/helpers.ts index 9611759fad271..82d50e7b9721b 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/helpers.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/helpers.ts @@ -5,6 +5,11 @@ * 2.0. */ +import type { EuiSelectableOption } from '@elastic/eui'; +import type { + CoverageOverviewRuleActivity, + CoverageOverviewRuleSource, +} from '../../../../../common/api/detection_engine'; import type { CoverageOverviewMitreTactic } from '../../../rule_management/model/coverage_overview/mitre_tactic'; import type { CoverageOverviewMitreTechnique } from '../../../rule_management/model/coverage_overview/mitre_technique'; import { coverageOverviewCardColorThresholds } from './constants'; @@ -22,3 +27,19 @@ export const getCardBackgroundColor = (value: number) => { } } }; + +export const extractSelected = < + T extends CoverageOverviewRuleSource | CoverageOverviewRuleActivity +>( + options: Array<{ checked?: string; label: T }> +): T[] => { + return options.filter((option) => option.checked === 'on').map((option) => option.label); +}; + +export const populateSelected = ( + allOptions: EuiSelectableOption[], + selected: string[] +): EuiSelectableOption[] => + allOptions.map((option) => + selected.includes(option.label) ? { ...option, checked: 'on' } : option + ); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/index.tsx new file mode 100644 index 0000000000000..c8b264435111a --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/index.tsx @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { SecuritySolutionPageWrapper } from '../../../../common/components/page_wrapper'; +import { SpyRoute } from '../../../../common/utils/route/spy_routes'; +import { SecurityPageName } from '../../../../app/types'; +import { CoverageOverviewDashboardContextProvider } from './coverage_overview_dashboard_context'; +import { CoverageOverviewDashboard } from './coverage_overview_dashboard'; + +export const CoverageOverviewPage = () => ( + <> + + + + + + + +); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/reducer.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/reducer.ts deleted file mode 100644 index cdafe0aa6b756..0000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/reducer.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export interface State { - showExpandedCells: boolean; -} - -export const initialState: State = { - showExpandedCells: false, -}; - -export interface Action { - type: 'setShowExpandedCells'; - value: boolean; -} - -export const createCoverageOverviewDashboardReducer = - () => - (state: State, action: Action): State => { - switch (action.type) { - case 'setShowExpandedCells': { - const { value } = action; - return { ...state, showExpandedCells: value }; - } - default: - return state; - } - }; diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/rule_activity_filter.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/rule_activity_filter.tsx new file mode 100644 index 0000000000000..0bb7e082e861e --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/rule_activity_filter.tsx @@ -0,0 +1,134 @@ +/* + * 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, useMemo, useState } from 'react'; +import type { EuiSelectableOption } from '@elastic/eui'; +import { + EuiPopover, + EuiFilterButton, + EuiSelectable, + EuiFilterGroup, + EuiPopoverTitle, + EuiButtonEmpty, + EuiPopoverFooter, +} from '@elastic/eui'; +import { css } from '@emotion/css'; +import type { CoverageOverviewRuleActivity } from '../../../../../common/api/detection_engine'; +import { + coverageOverviewFilterWidth, + ruleActivityFilterDefaultOptions, + ruleActivityFilterLabelMap, +} from './constants'; +import * as i18n from './translations'; +import { populateSelected, extractSelected } from './helpers'; + +export interface RuleActivityFilterComponentProps { + selected: CoverageOverviewRuleActivity[]; + onChange: (options: CoverageOverviewRuleActivity[]) => void; + isLoading: boolean; +} + +const RuleActivityFilterComponent = ({ + selected, + onChange, + isLoading, +}: RuleActivityFilterComponentProps) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const onButtonClick = useCallback(() => { + setIsPopoverOpen(!isPopoverOpen); + }, [isPopoverOpen]); + const closePopover = () => { + setIsPopoverOpen(false); + }; + + const numActiveFilters = useMemo(() => selected.length, [selected]); + + const options = populateSelected(ruleActivityFilterDefaultOptions, selected); + + const handleSelectableOnChange = useCallback( + (newOptions) => { + const formattedOptions = extractSelected(newOptions); + onChange(formattedOptions); + }, + [onChange] + ); + + const handleOnClear = useCallback(() => { + onChange([]); + }, [onChange]); + + const renderOptionLabel = (option: EuiSelectableOption) => + ruleActivityFilterLabelMap[option.label]; + + const button = useMemo( + () => ( + 0} + numActiveFilters={numActiveFilters} + > + {i18n.CoverageOverviewRuleActivityFilterLabel} + + ), + [isPopoverOpen, numActiveFilters, onButtonClick, isLoading] + ); + return ( + + + {i18n.CoverageOverviewFilterPopoverTitle} + + {(list) => ( +
+ {list} +
+ )} +
+ + + {i18n.CoverageOverviewFilterPopoverClearAll} + + +
+
+ ); +}; + +export const RuleActivityFilter = React.memo(RuleActivityFilterComponent); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/rule_source_filter.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/rule_source_filter.tsx new file mode 100644 index 0000000000000..c17af658672da --- /dev/null +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/rule_source_filter.tsx @@ -0,0 +1,133 @@ +/* + * 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, useMemo, useState } from 'react'; +import type { EuiSelectableOption } from '@elastic/eui'; +import { + EuiPopover, + EuiFilterButton, + EuiSelectable, + EuiFilterGroup, + EuiPopoverTitle, + EuiButtonEmpty, + EuiPopoverFooter, +} from '@elastic/eui'; +import { css } from '@emotion/css'; +import type { CoverageOverviewRuleSource } from '../../../../../common/api/detection_engine'; +import { + coverageOverviewFilterWidth, + ruleSourceFilterDefaultOptions, + ruleSourceFilterLabelMap, +} from './constants'; +import * as i18n from './translations'; +import { populateSelected, extractSelected } from './helpers'; + +export interface RuleSourceFilterComponentProps { + selected: CoverageOverviewRuleSource[]; + onChange: (options: CoverageOverviewRuleSource[]) => void; + isLoading: boolean; +} + +const RuleSourceFilterComponent = ({ + selected, + onChange, + isLoading, +}: RuleSourceFilterComponentProps) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const onButtonClick = useCallback(() => { + setIsPopoverOpen(!isPopoverOpen); + }, [isPopoverOpen]); + const closePopover = () => { + setIsPopoverOpen(false); + }; + + const numActiveFilters = useMemo(() => selected.length, [selected]); + + const options = populateSelected(ruleSourceFilterDefaultOptions, selected); + + const handleSelectableOnChange = useCallback( + (newOptions) => { + const formattedOptions = extractSelected(newOptions); + onChange(formattedOptions); + }, + [onChange] + ); + + const handleOnClear = useCallback(() => { + onChange([]); + }, [onChange]); + + const renderOptionLabel = (option: EuiSelectableOption) => ruleSourceFilterLabelMap[option.label]; + + const button = useMemo( + () => ( + 0} + numActiveFilters={numActiveFilters} + > + {i18n.CoverageOverviewRuleSourceFilterLabel} + + ), + [isPopoverOpen, numActiveFilters, onButtonClick, isLoading] + ); + return ( + + + {i18n.CoverageOverviewFilterPopoverTitle} + + {(list) => ( +
+ {list} +
+ )} +
+ + + {i18n.CoverageOverviewFilterPopoverClearAll} + + +
+
+ ); +}; + +export const RuleSourceFilter = React.memo(RuleSourceFilterComponent); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/shared_components/dashboard_legend.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/shared_components/dashboard_legend.tsx index 5a72efc13f1f3..7a68d68c7aebe 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/shared_components/dashboard_legend.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/shared_components/dashboard_legend.tsx @@ -45,6 +45,7 @@ export const CoverageOverviewLegend = () => { ? `\u003E${threshold}` : `${threshold}-${thresholdsMap[index - 1].threshold}` } ${i18n.CoverageOverviewLegendRulesLabel}`} + key={index} color={color} /> )), diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/shared_components/panel_metadata.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/shared_components/panel_rule_stats.tsx similarity index 77% rename from x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/shared_components/panel_metadata.tsx rename to x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/shared_components/panel_rule_stats.tsx index 06b995c34ba83..c07dd6f10f0af 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/shared_components/panel_metadata.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/shared_components/panel_rule_stats.tsx @@ -10,7 +10,7 @@ import { css, cx } from '@emotion/css'; import React from 'react'; import * as i18n from '../translations'; -export interface CoverageOverviewPanelMetadataProps { +export interface CoverageOverviewPanelRuleStatsProps { disabledRules: number; enabledRules: number; } @@ -21,12 +21,16 @@ const metadataLabelClass = css` text-overflow: ellipsis; `; -export const CoverageOverviewPanelMetadata = ({ +export const CoverageOverviewPanelRuleStats = ({ disabledRules, enabledRules, -}: CoverageOverviewPanelMetadataProps) => { +}: CoverageOverviewPanelRuleStatsProps) => { return ( - + @@ -34,7 +38,7 @@ export const CoverageOverviewPanelMetadata = ({ - + {disabledRules} @@ -47,7 +51,7 @@ export const CoverageOverviewPanelMetadata = ({
- + {enabledRules} diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/tactic_panel.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/tactic_panel.test.tsx deleted file mode 100644 index cddd257c130fa..0000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/tactic_panel.test.tsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { render } from '@testing-library/react'; -import React from 'react'; - -import { getMockCoverageOverviewMitreTactic } from '../../../rule_management/model/coverage_overview/__mocks__'; -import { TestProviders } from '../../../../common/mock'; -import { CoverageOverviewTacticPanel } from './tactic_panel'; -import type { CoverageOverviewMitreTactic } from '../../../rule_management/model/coverage_overview/mitre_tactic'; - -const renderTacticPanel = ( - tactic: CoverageOverviewMitreTactic = getMockCoverageOverviewMitreTactic() -) => { - return render( - - - - ); -}; - -describe('CoverageOverviewTacticPanel', () => { - test('it renders information correctly', () => { - const wrapper = renderTacticPanel(); - - expect(wrapper.getByTestId('coverageOverviewTacticPanel')).toBeInTheDocument(); - expect(wrapper.getByTestId('metadataDisabledRulesCount')).toHaveTextContent('1'); - expect(wrapper.getByTestId('metadataEnabledRulesCount')).toHaveTextContent('1'); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/tactic_panel.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/tactic_panel.tsx index 12431fe237617..e1d1749ca264f 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/tactic_panel.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/tactic_panel.tsx @@ -13,7 +13,7 @@ import type { CoverageOverviewMitreTactic } from '../../../rule_management/model import { coverageOverviewPanelWidth } from './constants'; import { getNumOfCoveredTechniques } from './helpers'; import * as i18n from './translations'; -import { CoverageOverviewPanelMetadata } from './shared_components/panel_metadata'; +import { CoverageOverviewPanelRuleStats } from './shared_components/panel_rule_stats'; export interface CoverageOverviewTacticPanelProps { tactic: CoverageOverviewMitreTactic; @@ -68,7 +68,7 @@ const CoverageOverviewTacticPanelComponent = ({ tactic }: CoverageOverviewTactic max={tactic.techniques.length} /> - diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel.test.tsx deleted file mode 100644 index 38e10e6299b8e..0000000000000 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel.test.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { render } from '@testing-library/react'; -import React from 'react'; - -import { getMockCoverageOverviewMitreTechnique } from '../../../rule_management/model/coverage_overview/__mocks__'; -import { TestProviders } from '../../../../common/mock'; -import { CoverageOverviewMitreTechniquePanel } from './technique_panel'; -import type { CoverageOverviewMitreTechnique } from '../../../rule_management/model/coverage_overview/mitre_technique'; - -const renderTechniquePanel = ( - technique: CoverageOverviewMitreTechnique = getMockCoverageOverviewMitreTechnique(), - isExpanded: boolean = false -) => { - return render( - - {}} - isPopoverOpen={false} - isExpanded={isExpanded} - /> - - ); -}; - -describe('CoverageOverviewMitreTechniquePanel', () => { - test('it renders collapsed view', () => { - const wrapper = renderTechniquePanel(); - - expect(wrapper.getByTestId('coverageOverviewTechniquePanel')).toBeInTheDocument(); - expect(wrapper.queryByTestId('coverageOverviewPanelMetadata')).not.toBeInTheDocument(); - }); - - test('it renders expanded view', () => { - const wrapper = renderTechniquePanel(getMockCoverageOverviewMitreTechnique(), true); - - expect(wrapper.getByTestId('coverageOverviewTechniquePanel')).toBeInTheDocument(); - expect(wrapper.getByTestId('coverageOverviewPanelMetadata')).toBeInTheDocument(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel.tsx index d8af376d32bab..8de089d62e298 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel.tsx @@ -11,7 +11,7 @@ import React, { memo, useCallback, useMemo } from 'react'; import type { CoverageOverviewMitreTechnique } from '../../../rule_management/model/coverage_overview/mitre_technique'; import { coverageOverviewPanelWidth } from './constants'; import { getCardBackgroundColor } from './helpers'; -import { CoverageOverviewPanelMetadata } from './shared_components/panel_metadata'; +import { CoverageOverviewPanelRuleStats } from './shared_components/panel_rule_stats'; import * as i18n from './translations'; export interface CoverageOverviewMitreTechniquePanelProps { @@ -80,7 +80,7 @@ const CoverageOverviewMitreTechniquePanelComponent = ({
{isExpanded && ( - diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel_popover.test.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel_popover.test.tsx index dba2b381deb88..a41cdad7abb58 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel_popover.test.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel_popover.test.tsx @@ -12,28 +12,52 @@ import { getMockCoverageOverviewMitreTechnique } from '../../../rule_management/ import { TestProviders } from '../../../../common/mock'; import type { CoverageOverviewMitreTechnique } from '../../../rule_management/model/coverage_overview/mitre_technique'; import { CoverageOverviewMitreTechniquePanelPopover } from './technique_panel_popover'; -import { useExecuteBulkAction } from '../../../rule_management/logic/bulk_actions/use_execute_bulk_action'; +import { useCoverageOverviewDashboardContext } from './coverage_overview_dashboard_context'; -jest.mock('../../../rule_management/logic/bulk_actions/use_execute_bulk_action'); +jest.mock('./coverage_overview_dashboard_context'); -const mockExecuteBulkAction = jest.fn(); - -(useExecuteBulkAction as jest.Mock).mockReturnValue({ - executeBulkAction: mockExecuteBulkAction, -}); +const mockEnableAllDisabled = jest.fn(); const renderTechniquePanelPopover = ( - technique: CoverageOverviewMitreTechnique = getMockCoverageOverviewMitreTechnique(), - isExpanded: boolean = false + technique: CoverageOverviewMitreTechnique = getMockCoverageOverviewMitreTechnique() ) => { return render( - + ); }; describe('CoverageOverviewMitreTechniquePanelPopover', () => { + beforeEach(() => { + (useCoverageOverviewDashboardContext as jest.Mock).mockReturnValue({ + state: { showExpandedCells: false }, + actions: { enableAllDisabled: mockEnableAllDisabled }, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('it renders panel with collapsed view', () => { + const wrapper = renderTechniquePanelPopover(); + + expect(wrapper.getByTestId('coverageOverviewTechniquePanel')).toBeInTheDocument(); + expect(wrapper.queryByTestId('coverageOverviewPanelRuleStats')).not.toBeInTheDocument(); + }); + + test('it renders panel with expanded view', () => { + (useCoverageOverviewDashboardContext as jest.Mock).mockReturnValue({ + state: { showExpandedCells: true }, + actions: { enableAllDisabled: mockEnableAllDisabled }, + }); + const wrapper = renderTechniquePanelPopover(); + + expect(wrapper.getByTestId('coverageOverviewTechniquePanel')).toBeInTheDocument(); + expect(wrapper.getByTestId('coverageOverviewPanelRuleStats')).toBeInTheDocument(); + }); + test('it renders all rules in correct areas', () => { const wrapper = renderTechniquePanelPopover(); @@ -64,7 +88,7 @@ describe('CoverageOverviewMitreTechniquePanelPopover', () => { fireEvent.click(wrapper.getByTestId('enableAllDisabledButton')); }); - expect(mockExecuteBulkAction).toHaveBeenCalledWith({ ids: ['rule-id'], type: 'enable' }); + expect(mockEnableAllDisabled).toHaveBeenCalledWith(['rule-id']); }); test('"Enable all disabled" button is disabled when there are no disabled rules', async () => { diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel_popover.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel_popover.tsx index 2a7ca6f6a22f3..9beae73a21c4c 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel_popover.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/technique_panel_popover.tsx @@ -21,48 +21,49 @@ import { } from '@elastic/eui'; import { css, cx } from '@emotion/css'; import React, { memo, useCallback, useMemo, useState } from 'react'; -import { BulkActionType } from '../../../../../common/api/detection_engine'; -import { useExecuteBulkAction } from '../../../rule_management/logic/bulk_actions/use_execute_bulk_action'; import type { CoverageOverviewMitreTechnique } from '../../../rule_management/model/coverage_overview/mitre_technique'; import { getNumOfCoveredSubtechniques } from './helpers'; import { CoverageOverviewRuleListHeader } from './shared_components/popover_list_header'; import { CoverageOverviewMitreTechniquePanel } from './technique_panel'; import * as i18n from './translations'; import { RuleLink } from '../../components/rules_table/use_columns'; +import { useCoverageOverviewDashboardContext } from './coverage_overview_dashboard_context'; export interface CoverageOverviewMitreTechniquePanelPopoverProps { technique: CoverageOverviewMitreTechnique; - isExpanded: boolean; } const CoverageOverviewMitreTechniquePanelPopoverComponent = ({ technique, - isExpanded, }: CoverageOverviewMitreTechniquePanelPopoverProps) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [isEnableButtonLoading, setIsDisableButtonLoading] = useState(false); const closePopover = useCallback(() => setIsPopoverOpen(false), []); const coveredSubtechniques = useMemo(() => getNumOfCoveredSubtechniques(technique), [technique]); - const { executeBulkAction } = useExecuteBulkAction(); const isEnableButtonDisabled = useMemo( () => technique.disabledRules.length === 0, [technique.disabledRules.length] ); + const { + state: { showExpandedCells }, + actions: { enableAllDisabled }, + } = useCoverageOverviewDashboardContext(); + const handleEnableAllDisabled = useCallback(async () => { setIsDisableButtonLoading(true); const ruleIds = technique.disabledRules.map((rule) => rule.id); - await executeBulkAction({ type: BulkActionType.enable, ids: ruleIds }); + await enableAllDisabled(ruleIds); setIsDisableButtonLoading(false); closePopover(); - }, [closePopover, executeBulkAction, technique.disabledRules]); + }, [closePopover, enableAllDisabled, technique.disabledRules]); const TechniquePanel = ( ); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/translations.ts b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/translations.ts index ce4587fb01aea..b4aa93f2bcc02 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/translations.ts +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/translations.ts @@ -105,3 +105,67 @@ export const CoverageOverviewLegendRulesLabel = i18n.translate( defaultMessage: 'rules', } ); + +export const CoverageOverviewEnabledRuleActivity = i18n.translate( + 'xpack.securitySolution.coverageOverviewDashboard.enabledRuleActivity', + { + defaultMessage: 'Enabled rules', + } +); + +export const CoverageOverviewDisabledRuleActivity = i18n.translate( + 'xpack.securitySolution.coverageOverviewDashboard.disabledRuleActivity', + { + defaultMessage: 'Disabled rules', + } +); + +export const CoverageOverviewElasticRuleSource = i18n.translate( + 'xpack.securitySolution.coverageOverviewDashboard.elasticRuleSource', + { + defaultMessage: 'Elastic rules', + } +); + +export const CoverageOverviewCustomRuleSource = i18n.translate( + 'xpack.securitySolution.coverageOverviewDashboard.customRuleSource', + { + defaultMessage: 'Custom rules', + } +); + +export const CoverageOverviewRuleActivityFilterLabel = i18n.translate( + 'xpack.securitySolution.coverageOverviewDashboard.ruleActivityFilterLabel', + { + defaultMessage: 'Installed rule status', + } +); + +export const CoverageOverviewRuleSourceFilterLabel = i18n.translate( + 'xpack.securitySolution.coverageOverviewDashboard.ruleSourceFilterLabel', + { + defaultMessage: 'Installed rule type', + } +); + +export const CoverageOverviewSearchBarPlaceholder = i18n.translate( + 'xpack.securitySolution.coverageOverviewDashboard.searchBarPlaceholder', + { + defaultMessage: + 'Search for the tactic, technique (e.g.,"defence evasion" or "TA0005") or rule name, index pattern (e.g.,"filebeat-*")', + } +); + +export const CoverageOverviewFilterPopoverTitle = i18n.translate( + 'xpack.securitySolution.coverageOverviewDashboard.filterPopoverTitle', + { + defaultMessage: 'Select to view on framework', + } +); + +export const CoverageOverviewFilterPopoverClearAll = i18n.translate( + 'xpack.securitySolution.coverageOverviewDashboard.filterPopoverClearAll', + { + defaultMessage: 'Clear all', + } +); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx index 968f63f58c9ff..d996ee4e49592 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/schema.tsx @@ -39,7 +39,6 @@ import { THREAT_MATCH_INDEX_HELPER_TEXT, THREAT_MATCH_REQUIRED, THREAT_MATCH_EMPTIES, - SAVED_QUERY_REQUIRED, } from './translations'; export const schema: FormSchema = { @@ -147,7 +146,10 @@ export const schema: FormSchema = { return undefined; } if (savedId) { - return { code: 'ERR_FIELD_MISSING', path, message: SAVED_QUERY_REQUIRED }; + // Ignore field validation error in this case. + // Instead, we show the error toast when saved query object does not exist. + // https://github.com/elastic/kibana/issues/159060 + return undefined; } const message = isEqlRule(formData.ruleType) ? EQL_QUERY_REQUIRED : CUSTOM_QUERY_REQUIRED; return { code: 'ERR_FIELD_MISSING', path, message }; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/translations.tsx index 28253e34550e2..12b82d427edb1 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/translations.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_define_rule/translations.tsx @@ -14,13 +14,6 @@ export const CUSTOM_QUERY_REQUIRED = i18n.translate( } ); -export const SAVED_QUERY_REQUIRED = i18n.translate( - 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.savedQueryFieldRequiredError', - { - defaultMessage: 'Failed to load the saved query. Select a new one or add a custom query.', - } -); - export const EQL_QUERY_REQUIRED = i18n.translate( 'xpack.securitySolution.detectionEngine.createRule.stepDefineRule.eqlQueryFieldRequiredError', { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts index f3fa972785673..43b35ec683963 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts @@ -275,6 +275,7 @@ describe('policy details: ', () => { license_uid: '', cluster_name: '', cluster_uuid: '', + serverless: false, }, windows: { events: { diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_metadata_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_metadata_services.ts index b46eac58e24b3..a69f348c366eb 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_metadata_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/endpoint_metadata_services.ts @@ -10,6 +10,7 @@ import type { KbnClient } from '@kbn/test'; import type { WriteResponseBase } from '@elastic/elasticsearch/lib/api/types'; import { clone, merge } from 'lodash'; import type { DeepPartial } from 'utility-types'; +import { catchAxiosErrorFormatAndThrow } from './format_axios_error'; import type { GetMetadataListRequestQuery } from '../../../common/api/endpoint'; import { resolvePathVariables } from '../../../public/common/utils/resolve_path_variables'; import { @@ -27,13 +28,15 @@ export const fetchEndpointMetadata = async ( agentId: string ): Promise => { return ( - await kbnClient.request({ - method: 'GET', - path: resolvePathVariables(HOST_METADATA_GET_ROUTE, { id: agentId }), - headers: { - 'Elastic-Api-Version': '2023-10-31', - }, - }) + await kbnClient + .request({ + method: 'GET', + path: resolvePathVariables(HOST_METADATA_GET_ROUTE, { id: agentId }), + headers: { + 'Elastic-Api-Version': '2023-10-31', + }, + }) + .catch(catchAxiosErrorFormatAndThrow) ).data; }; @@ -42,18 +45,20 @@ export const fetchEndpointMetadataList = async ( { page = 0, pageSize = 100, ...otherOptions }: Partial = {} ): Promise => { return ( - await kbnClient.request({ - method: 'GET', - path: HOST_METADATA_LIST_ROUTE, - headers: { - 'Elastic-Api-Version': '2023-10-31', - }, - query: { - page, - pageSize, - ...otherOptions, - }, - }) + await kbnClient + .request({ + method: 'GET', + path: HOST_METADATA_LIST_ROUTE, + headers: { + 'Elastic-Api-Version': '2023-10-31', + }, + query: { + page, + pageSize, + ...otherOptions, + }, + }) + .catch(catchAxiosErrorFormatAndThrow) ).data; }; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts index ea788063b572c..d81fa0c294706 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/fleet_services.ts @@ -35,6 +35,7 @@ import type { } from '@kbn/fleet-plugin/common/types'; import nodeFetch from 'node-fetch'; import semver from 'semver'; +import { catchAxiosErrorFormatAndThrow } from './format_axios_error'; import { FleetAgentGenerator } from '../../../common/endpoint/data_generators/fleet_agent_generator'; const fleetGenerator = new FleetAgentGenerator(); @@ -106,6 +107,7 @@ export const fetchFleetAgents = async ( path: AGENT_API_ROUTES.LIST_PATTERN, query: options, }) + .catch(catchAxiosErrorFormatAndThrow) .then((response) => response.data); }; @@ -161,6 +163,7 @@ export const fetchFleetServerUrl = async (kbnClient: KbnClient): Promise response.data); // TODO:PT need to also pull in the Proxies and use that instead if defiend for url @@ -195,6 +198,7 @@ export const fetchAgentPolicyEnrollmentKey = async ( path: enrollmentAPIKeyRouteService.getListPath(), query: { kuery: `policy_id: "${agentPolicyId}"` }, }) + .catch(catchAxiosErrorFormatAndThrow) .then((response) => response.data.items[0]); if (!apiKey) { @@ -219,6 +223,7 @@ export const fetchAgentPolicyList = async ( path: agentPolicyRouteService.getListPath(), query: options, }) + .catch(catchAxiosErrorFormatAndThrow) .then((response) => response.data); }; @@ -369,11 +374,13 @@ export const unEnrollFleetAgent = async ( agentId: string, force = false ): Promise => { - const { data } = await kbnClient.request({ - method: 'POST', - path: agentRouteService.getUnenrollPath(agentId), - body: { revoke: force }, - }); + const { data } = await kbnClient + .request({ + method: 'POST', + path: agentRouteService.getUnenrollPath(agentId), + body: { revoke: force }, + }) + .catch(catchAxiosErrorFormatAndThrow); return data; }; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/format_axios_error.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/format_axios_error.ts new file mode 100644 index 0000000000000..ccb3dc125f561 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/format_axios_error.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AxiosError } from 'axios'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export class FormattedAxiosError extends Error { + public readonly request: { + method: string; + url: string; + data: unknown; + }; + public readonly response: { + status: number; + statusText: string; + data: any; + }; + + constructor(axiosError: AxiosError) { + super(axiosError.message); + + this.request = { + method: axiosError.config.method ?? '?', + url: axiosError.config.url ?? '?', + data: axiosError.config.data ?? '', + }; + + this.response = { + status: axiosError?.response?.status ?? 0, + statusText: axiosError?.response?.statusText ?? '', + data: axiosError?.response?.data, + }; + + this.name = this.constructor.name; + } + + toJSON() { + return { + message: this.message, + request: this.request, + response: this.response, + }; + } + + toString() { + return JSON.stringify(this.toJSON(), null, 2); + } +} + +/** + * Used with `promise.catch()`, it will format the Axios error to a new error and will re-throw + * @param error + */ +export const catchAxiosErrorFormatAndThrow = (error: Error): never => { + if (error instanceof AxiosError) { + throw new FormattedAxiosError(error); + } + + throw error; +}; diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/random_policy_id_generator.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/random_policy_id_generator.ts index acf6f53bc785e..3b494d3bfe9cb 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/random_policy_id_generator.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/random_policy_id_generator.ts @@ -12,6 +12,7 @@ import { PACKAGE_POLICY_API_ROUTES, PACKAGE_POLICY_SAVED_OBJECT_TYPE, } from '@kbn/fleet-plugin/common/constants'; +import { catchAxiosErrorFormatAndThrow } from './format_axios_error'; import { indexFleetEndpointPolicy } from '../../../common/endpoint/data_loaders/index_fleet_endpoint_policy'; import { setupFleetForEndpoint } from '../../../common/endpoint/data_loaders/setup_fleet_for_endpoint'; import type { GetPolicyListResponse } from '../../../public/management/pages/policy/types'; @@ -20,14 +21,16 @@ import { getEndpointPackageInfo } from '../../../common/endpoint/utils/package'; const fetchEndpointPolicies = ( kbnClient: KbnClient ): Promise> => { - return kbnClient.request({ - method: 'GET', - path: PACKAGE_POLICY_API_ROUTES.LIST_PATTERN, - query: { - perPage: 100, - kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: endpoint`, - }, - }); + return kbnClient + .request({ + method: 'GET', + path: PACKAGE_POLICY_API_ROUTES.LIST_PATTERN, + query: { + perPage: 100, + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: endpoint`, + }, + }) + .catch(catchAxiosErrorFormatAndThrow); }; // Setup a list of real endpoint policies and return a method to randomly select one diff --git a/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts b/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts index a2a24aec142b8..a3ad237fc3bcb 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/common/stack_services.ts @@ -11,6 +11,7 @@ import { KbnClient } from '@kbn/test'; import type { StatusResponse } from '@kbn/core-status-common-internal'; import pRetry from 'p-retry'; import nodeFetch from 'node-fetch'; +import { catchAxiosErrorFormatAndThrow } from './format_axios_error'; import { isLocalhost } from './is_localhost'; import { getLocalhostRealIp } from './localhost_services'; import { createSecuritySuperuser } from './security_user_services'; @@ -189,10 +190,12 @@ export const createKbnClient = ({ */ export const fetchStackVersion = async (kbnClient: KbnClient): Promise => { const status = ( - await kbnClient.request({ - method: 'GET', - path: '/api/status', - }) + await kbnClient + .request({ + method: 'GET', + path: '/api/status', + }) + .catch(catchAxiosErrorFormatAndThrow) ).data; if (!status?.version?.number) { diff --git a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/fleet_server.ts b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/fleet_server.ts index a7058501f125c..f9d88382d81c7 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/fleet_server.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/endpoint_agent_runner/fleet_server.ts @@ -36,6 +36,8 @@ import type { PostFleetServerHostsResponse, } from '@kbn/fleet-plugin/common/types/rest_spec/fleet_server_hosts'; import chalk from 'chalk'; +import type { FormattedAxiosError } from '../common/format_axios_error'; +import { catchAxiosErrorFormatAndThrow } from '../common/format_axios_error'; import { isLocalhost } from '../common/is_localhost'; import { dump } from './utils'; import { fetchFleetServerUrl, waitForHostToEnroll } from '../common/fleet_services'; @@ -243,7 +245,7 @@ export const startFleetServerWithDocker = async ({ containerId = (await execa('docker', dockerArgs)).stdout; - const fleetServerAgent = await waitForHostToEnroll(kbnClient, containerName); + const fleetServerAgent = await waitForHostToEnroll(kbnClient, containerName, 120000); log.verbose(`Fleet server enrolled agent:\n${JSON.stringify(fleetServerAgent, null, 2)}`); @@ -313,11 +315,13 @@ const configureFleetIfNeeded = async () => { log.info(`Updating Fleet Settings for Output [${output.name} (${id})]`); - await kbnClient.request({ - method: 'PUT', - path: outputRoutesService.getUpdatePath(id), - body: update, - }); + await kbnClient + .request({ + method: 'PUT', + path: outputRoutesService.getUpdatePath(id), + body: update, + }) + .catch(catchAxiosErrorFormatAndThrow); } } } @@ -354,6 +358,25 @@ const addFleetServerHostToFleetSettings = async ( path: fleetServerHostsRoutesService.getCreatePath(), body: newFleetHostEntry, }) + .catch(catchAxiosErrorFormatAndThrow) + .catch((error: FormattedAxiosError) => { + if ( + error.response.status === 403 && + ((error.response?.data?.message as string) ?? '').includes('disabled') + ) { + log.error(`Update failed with [403: ${error.response.data.message}]. + +${chalk.red('Are you running this utility against a Serverless project?')} +If so, the following entry should be added to your local +'config/serverless.[project_type].dev.yml' (ex. 'serverless.security.dev.yml'): + +${chalk.bold(chalk.cyan('xpack.fleet.internal.fleetServerStandalone: false'))} + +`); + } + + throw error; + }) .then((response) => response.data); log.verbose(item); diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/index.ts b/x-pack/plugins/security_solution/scripts/openapi/generate.js similarity index 69% rename from x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/index.ts rename to x-pack/plugins/security_solution/scripts/openapi/generate.js index 324ce06e2d418..bd88357a3754d 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/coverage_overview/index.ts +++ b/x-pack/plugins/security_solution/scripts/openapi/generate.js @@ -5,4 +5,7 @@ * 2.0. */ -export { CoverageOverviewPage } from './coverage_overview_page'; +require('../../../../../src/setup_node_env'); +const { generate } = require('./openapi_generator'); + +generate(); diff --git a/x-pack/plugins/security_solution/scripts/openapi/lib/fix_eslint.ts b/x-pack/plugins/security_solution/scripts/openapi/lib/fix_eslint.ts new file mode 100644 index 0000000000000..23d8bf540f731 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/openapi/lib/fix_eslint.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import execa from 'execa'; +import { resolve } from 'path'; + +const KIBANA_ROOT = resolve(__dirname, '../../../../../../'); + +export async function fixEslint(path: string) { + await execa('npx', ['eslint', '--fix', path], { + // Need to run eslint from the Kibana root directory, otherwise it will not + // be able to pick up the right config + cwd: KIBANA_ROOT, + }); +} diff --git a/x-pack/plugins/spaces/public/advanced_settings/index.ts b/x-pack/plugins/security_solution/scripts/openapi/lib/format_output.ts similarity index 65% rename from x-pack/plugins/spaces/public/advanced_settings/index.ts rename to x-pack/plugins/security_solution/scripts/openapi/lib/format_output.ts index 64220f838650b..6c374aa1f06d2 100644 --- a/x-pack/plugins/spaces/public/advanced_settings/index.ts +++ b/x-pack/plugins/security_solution/scripts/openapi/lib/format_output.ts @@ -5,4 +5,8 @@ * 2.0. */ -export { AdvancedSettingsService } from './advanced_settings_service'; +import execa from 'execa'; + +export async function formatOutput(path: string) { + await execa('npx', ['prettier', '--write', path]); +} diff --git a/x-pack/plugins/security_solution/scripts/openapi/lib/remove_gen_artifacts.ts b/x-pack/plugins/security_solution/scripts/openapi/lib/remove_gen_artifacts.ts new file mode 100644 index 0000000000000..3cbf421b8c94b --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/openapi/lib/remove_gen_artifacts.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import fs from 'fs/promises'; +import globby from 'globby'; +import { resolve } from 'path'; + +/** + * Removes any *.gen.ts files from the target directory + * + * @param folderPath target directory + */ +export async function removeGenArtifacts(folderPath: string) { + const artifactsPath = await globby([resolve(folderPath, './**/*.gen.ts')]); + + await Promise.all(artifactsPath.map((artifactPath) => fs.unlink(artifactPath))); +} diff --git a/x-pack/plugins/security_solution/scripts/openapi/openapi_generator.ts b/x-pack/plugins/security_solution/scripts/openapi/openapi_generator.ts new file mode 100644 index 0000000000000..272e62061c6a4 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/openapi/openapi_generator.ts @@ -0,0 +1,77 @@ +/* + * 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. + */ + +/* eslint-disable no-console */ + +import SwaggerParser from '@apidevtools/swagger-parser'; +import chalk from 'chalk'; +import fs from 'fs/promises'; +import globby from 'globby'; +import { resolve } from 'path'; +import { fixEslint } from './lib/fix_eslint'; +import { formatOutput } from './lib/format_output'; +import { removeGenArtifacts } from './lib/remove_gen_artifacts'; +import { getApiOperationsList } from './parsers/get_api_operations_list'; +import { getComponents } from './parsers/get_components'; +import { getImportsMap } from './parsers/get_imports_map'; +import type { OpenApiDocument } from './parsers/openapi_types'; +import { initTemplateService } from './template_service/template_service'; + +const ROOT_SECURITY_SOLUTION_FOLDER = resolve(__dirname, '../..'); +const COMMON_API_FOLDER = resolve(ROOT_SECURITY_SOLUTION_FOLDER, './common/api'); +const SCHEMA_FILES_GLOB = resolve(ROOT_SECURITY_SOLUTION_FOLDER, './**/*.schema.yaml'); +const GENERATED_ARTIFACTS_GLOB = resolve(COMMON_API_FOLDER, './**/*.gen.ts'); + +export const generate = async () => { + console.log(chalk.bold(`Generating API route schemas`)); + console.log(chalk.bold(`Working directory: ${chalk.underline(COMMON_API_FOLDER)}`)); + + console.log(`👀 Searching for schemas`); + const schemaPaths = await globby([SCHEMA_FILES_GLOB]); + + console.log(`🕵️‍♀️ Found ${schemaPaths.length} schemas, parsing`); + const parsedSchemas = await Promise.all( + schemaPaths.map(async (schemaPath) => { + const parsedSchema = (await SwaggerParser.parse(schemaPath)) as OpenApiDocument; + return { schemaPath, parsedSchema }; + }) + ); + + console.log(`🧹 Cleaning up any previously generated artifacts`); + await removeGenArtifacts(COMMON_API_FOLDER); + + console.log(`🪄 Generating new artifacts`); + const TemplateService = await initTemplateService(); + await Promise.all( + parsedSchemas.map(async ({ schemaPath, parsedSchema }) => { + const components = getComponents(parsedSchema); + const apiOperations = getApiOperationsList(parsedSchema); + const importsMap = getImportsMap(parsedSchema); + + // If there are no operations or components to generate, skip this file + const shouldGenerate = apiOperations.length > 0 || components !== undefined; + if (!shouldGenerate) { + return; + } + + const result = TemplateService.compileTemplate('schemas', { + components, + apiOperations, + importsMap, + }); + + // Write the generation result to disk + await fs.writeFile(schemaPath.replace('.schema.yaml', '.gen.ts'), result); + }) + ); + + // Format the output folder using prettier as the generator produces + // unformatted code and fix any eslint errors + console.log(`💅 Formatting output`); + await formatOutput(GENERATED_ARTIFACTS_GLOB); + await fixEslint(GENERATED_ARTIFACTS_GLOB); +}; diff --git a/x-pack/plugins/security_solution/scripts/openapi/parsers/get_api_operations_list.ts b/x-pack/plugins/security_solution/scripts/openapi/parsers/get_api_operations_list.ts new file mode 100644 index 0000000000000..c9d9a75c07854 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/openapi/parsers/get_api_operations_list.ts @@ -0,0 +1,117 @@ +/* + * 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 { OpenAPIV3 } from 'openapi-types'; +import type { NormalizedOperation, ObjectSchema, OpenApiDocument } from './openapi_types'; + +const HTTP_METHODS = Object.values(OpenAPIV3.HttpMethods); + +export function getApiOperationsList(parsedSchema: OpenApiDocument): NormalizedOperation[] { + const operations: NormalizedOperation[] = Object.entries(parsedSchema.paths).flatMap( + ([path, pathDefinition]) => { + return HTTP_METHODS.flatMap((method) => { + const operation = pathDefinition?.[method]; + if (operation?.['x-codegen-enabled'] !== true) { + // Skip the operation if it's not enabled for codegen + return []; + } + + // Convert the query parameters to a schema object. In OpenAPI spec the + // query and path params are different from the request body, we want to + // convert them to a single schema format to simplify their usage in the + // templates + const params: Record<'query' | 'path', ObjectSchema> = { + query: { + type: 'object', + properties: {}, + required: [], + }, + path: { + type: 'object', + properties: {}, + required: [], + }, + }; + + operation.parameters?.forEach((parameter) => { + if ('name' in parameter && (parameter.in === 'query' || parameter.in === 'path')) { + params[parameter.in].properties[parameter.name] = { + ...parameter.schema, + description: parameter.description, + }; + + if (parameter.required) { + params[parameter.in].required.push(parameter.name); + } + } + }); + + const requestParams = Object.keys(params.path.properties).length ? params.path : undefined; + const requestQuery = Object.keys(params.query.properties).length ? params.query : undefined; + + // We don't use $ref in responses or request bodies currently, so we + // throw an error if we encounter one to narrow down the types. The + // support might be added in the future if needed. + if ('$ref' in operation.responses?.['200']) { + throw new Error( + `Cannot generate response for ${method} ${path}: $ref in response is not supported` + ); + } + const response = operation.responses?.['200']?.content?.['application/json']?.schema; + + if (operation.requestBody && '$ref' in operation.requestBody) { + throw new Error( + `Cannot generate request for ${method} ${path}: $ref in request body is not supported` + ); + } + const requestBody = operation.requestBody?.content?.['application/json']?.schema; + + const { operationId, description, tags, deprecated } = operation; + + // Operation ID is used as a prefix for the generated function names, + // runtime schemas, etc. So it must be unique and not empty + if (!operationId) { + throw new Error(`Missing operationId for ${method} ${path}`); + } + + return { + path, + method, + operationId, + description, + tags, + deprecated, + requestParams, + requestQuery, + requestBody, + response, + }; + }); + } + ); + + // Check that all operation IDs are unique + const operationIdOccurrences = operations.reduce((acc, operation) => { + acc[operation.operationId] = (acc[operation.operationId] ?? 0) + 1; + return acc; + }, {} as Record); + const duplicateOperationIds = Object.entries(operationIdOccurrences).filter( + ([, count]) => count > 1 + ); + if (duplicateOperationIds.length) { + throw new Error( + `Operation IDs must be unique, found duplicates: ${duplicateOperationIds + .map(([operationId, count]) => `${operationId} (${count})`) + .join(', ')}` + ); + } + + // Sort the operations by operationId to make the generated code more stable + operations.sort((a, b) => a.operationId.localeCompare(b.operationId)); + + return operations; +} diff --git a/x-pack/plugins/security_solution/scripts/openapi/parsers/get_components.ts b/x-pack/plugins/security_solution/scripts/openapi/parsers/get_components.ts new file mode 100644 index 0000000000000..5b3fef72905c0 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/openapi/parsers/get_components.ts @@ -0,0 +1,15 @@ +/* + * 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 { OpenApiDocument } from './openapi_types'; + +export function getComponents(parsedSchema: OpenApiDocument) { + if (parsedSchema.components?.['x-codegen-enabled'] === false) { + return undefined; + } + return parsedSchema.components; +} diff --git a/x-pack/plugins/security_solution/scripts/openapi/parsers/get_imports_map.ts b/x-pack/plugins/security_solution/scripts/openapi/parsers/get_imports_map.ts new file mode 100644 index 0000000000000..8e068b61ba034 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/openapi/parsers/get_imports_map.ts @@ -0,0 +1,77 @@ +/* + * 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 { uniq } from 'lodash'; +import type { OpenApiDocument } from './openapi_types'; + +export interface ImportsMap { + [importPath: string]: string[]; +} + +/** + * Traverse the OpenAPI document, find all external references, and return a map + * of import paths and imported symbols + * + * @param parsedSchema Parsed OpenAPI document + * @returns A map of import paths to symbols to import + */ +export const getImportsMap = (parsedSchema: OpenApiDocument): ImportsMap => { + const importMap: Record = {}; // key: import path, value: list of symbols to import + const refs = findRefs(parsedSchema); + refs.forEach((ref) => { + const refParts = ref.split('#/components/schemas/'); + const importedSymbol = refParts[1]; + let importPath = refParts[0]; + if (importPath) { + importPath = importPath.replace('.schema.yaml', '.gen'); + const currentSymbols = importMap[importPath] ?? []; + importMap[importPath] = uniq([...currentSymbols, importedSymbol]); + } + }); + + return importMap; +}; + +/** + * Check if an object has a $ref property + * + * @param obj Any object + * @returns True if the object has a $ref property + */ +const hasRef = (obj: unknown): obj is { $ref: string } => { + return typeof obj === 'object' && obj !== null && '$ref' in obj; +}; + +/** + * Traverse the OpenAPI document recursively and find all references + * + * @param obj Any object + * @returns A list of external references + */ +function findRefs(obj: unknown): string[] { + const refs: string[] = []; + + function search(element: unknown) { + if (typeof element === 'object' && element !== null) { + if (hasRef(element)) { + refs.push(element.$ref); + } + + Object.values(element).forEach((value) => { + if (Array.isArray(value)) { + value.forEach(search); + } else { + search(value); + } + }); + } + } + + search(obj); + + return refs; +} diff --git a/x-pack/plugins/security_solution/scripts/openapi/parsers/openapi_types.ts b/x-pack/plugins/security_solution/scripts/openapi/parsers/openapi_types.ts new file mode 100644 index 0000000000000..2449f34fa4b76 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/openapi/parsers/openapi_types.ts @@ -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 type { OpenAPIV3 } from 'openapi-types'; + +interface AdditionalProperties { + /** + * Whether or not the route and its schemas should be generated + */ + 'x-codegen-enabled'?: boolean; +} + +export type OpenApiDocument = OpenAPIV3.Document; + +// Override the OpenAPI types to add the x-codegen-enabled property to the +// components object. +declare module 'openapi-types' { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace OpenAPIV3 { + interface ComponentsObject { + 'x-codegen-enabled'?: boolean; + } + } +} + +/** + * OpenAPI types do not have a dedicated type for objects, so we need to create + * to use for path and query parameters + */ +export interface ObjectSchema { + type: 'object'; + required: string[]; + description?: string; + properties: { + [name: string]: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject; + }; +} + +/** + * The normalized operation object that is used in the templates + */ +export interface NormalizedOperation { + path: string; + method: OpenAPIV3.HttpMethods; + operationId: string; + description?: string; + tags?: string[]; + deprecated?: boolean; + requestParams?: ObjectSchema; + requestQuery?: ObjectSchema; + requestBody?: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject; + response?: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject; +} diff --git a/x-pack/plugins/security_solution/scripts/openapi/template_service/register_helpers.ts b/x-pack/plugins/security_solution/scripts/openapi/template_service/register_helpers.ts new file mode 100644 index 0000000000000..b3bb02f7743c8 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/openapi/template_service/register_helpers.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type Handlebars from '@kbn/handlebars'; +import { snakeCase, camelCase } from 'lodash'; + +export function registerHelpers(handlebarsInstance: typeof Handlebars) { + handlebarsInstance.registerHelper('concat', (...args) => { + const values = args.slice(0, -1) as unknown[]; + return values.join(''); + }); + handlebarsInstance.registerHelper('parseRef', (refName: string) => { + return refName.split('/').pop(); + }); + handlebarsInstance.registerHelper('snakeCase', snakeCase); + handlebarsInstance.registerHelper('camelCase', camelCase); + handlebarsInstance.registerHelper('toJSON', (value: unknown) => { + return JSON.stringify(value); + }); + handlebarsInstance.registerHelper('includes', (array: unknown, value: unknown) => { + if (!Array.isArray(array)) { + return false; + } + return array.includes(value); + }); + handlebarsInstance.registerHelper('or', (...args) => { + // Last arguments is the handlebars context, so we ignore it + return args.slice(0, -1).some((arg) => arg); + }); + handlebarsInstance.registerHelper('eq', (a, b) => { + return a === b; + }); + handlebarsInstance.registerHelper('defined', (val) => { + return val !== undefined; + }); + /** + * Check if the OpenAPI schema is unknown + */ + handlebarsInstance.registerHelper('isUnknown', (val: object) => { + return !('type' in val || '$ref' in val || 'anyOf' in val || 'oneOf' in val || 'allOf' in val); + }); + handlebarsInstance.registerHelper('isEmpty', (val) => { + if (Array.isArray(val)) { + return val.length === 0; + } + if (typeof val === 'object') { + return Object.keys(val).length === 0; + } + return val === undefined || val === null || val === ''; + }); +} diff --git a/x-pack/plugins/security_solution/scripts/openapi/template_service/register_templates.ts b/x-pack/plugins/security_solution/scripts/openapi/template_service/register_templates.ts new file mode 100644 index 0000000000000..fa39b52d99471 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/openapi/template_service/register_templates.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type Handlebars from '@kbn/handlebars'; +import fs from 'fs/promises'; +import path from 'path'; + +export async function registerTemplates( + templatesPath: string, + handlebarsInstance: typeof Handlebars +) { + const files = await fs.readdir(templatesPath); + + const fileContentsPromises = files.map(async (file) => { + const filePath = path.join(templatesPath, file); + const content = await fs.readFile(filePath, 'utf-8'); + return { fileName: path.parse(file).name, content }; + }); + + const fileContents = await Promise.all(fileContentsPromises); + + fileContents.forEach(({ fileName, content }) => { + handlebarsInstance.registerPartial(fileName, content); + }); + + return Object.fromEntries(fileContents.map(({ fileName, content }) => [fileName, content])); +} diff --git a/x-pack/plugins/security_solution/scripts/openapi/template_service/template_service.ts b/x-pack/plugins/security_solution/scripts/openapi/template_service/template_service.ts new file mode 100644 index 0000000000000..becb02bb54ebe --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/openapi/template_service/template_service.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import Handlebars from 'handlebars'; +import type { OpenAPIV3 } from 'openapi-types'; +import { resolve } from 'path'; +import type { ImportsMap } from '../parsers/get_imports_map'; +import type { NormalizedOperation } from '../parsers/openapi_types'; +import { registerHelpers } from './register_helpers'; +import { registerTemplates } from './register_templates'; + +export interface TemplateContext { + importsMap: ImportsMap; + apiOperations: NormalizedOperation[]; + components: OpenAPIV3.ComponentsObject | undefined; +} + +export type TemplateName = 'schemas'; + +export interface ITemplateService { + compileTemplate: (templateName: TemplateName, context: TemplateContext) => string; +} + +/** + * Initialize the template service. This service encapsulates the handlebars + * initialization logic and provides helper methods for compiling templates. + */ +export const initTemplateService = async (): Promise => { + // Create a handlebars instance and register helpers and partials + const handlebars = Handlebars.create(); + registerHelpers(handlebars); + const templates = await registerTemplates(resolve(__dirname, './templates'), handlebars); + + return { + compileTemplate: (templateName: TemplateName, context: TemplateContext) => { + return handlebars.compile(templates[templateName])(context); + }, + }; +}; diff --git a/x-pack/plugins/security_solution/scripts/openapi/template_service/templates/disclaimer.handlebars b/x-pack/plugins/security_solution/scripts/openapi/template_service/templates/disclaimer.handlebars new file mode 100644 index 0000000000000..4be0a93d1b79e --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/openapi/template_service/templates/disclaimer.handlebars @@ -0,0 +1,5 @@ +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator `yarn openapi:generate`. + */ + \ No newline at end of file diff --git a/x-pack/plugins/security_solution/scripts/openapi/template_service/templates/schema_item.handlebars b/x-pack/plugins/security_solution/scripts/openapi/template_service/templates/schema_item.handlebars new file mode 100644 index 0000000000000..87ce8e58105c7 --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/openapi/template_service/templates/schema_item.handlebars @@ -0,0 +1,98 @@ +{{~#if type~}} + {{~> (concat "type_" type)~}} + {{~#if nullable}}.nullable(){{/if~}} + {{~#if (eq requiredBool false)}}.optional(){{/if~}} + {{~#if (defined default)}}.default({{{toJSON default}}}){{/if~}} +{{~/if~}} + +{{~#if $ref~}} + {{parseRef $ref}} + {{~#if nullable}}.nullable(){{/if~}} + {{~#if (eq requiredBool false)}}.optional(){{/if~}} + {{~#if (defined default)}}.default({{{toJSON default}}}){{/if~}} +{{~/if~}} + +{{~#if allOf~}} + {{~#each allOf~}} + {{~#if @first~}} + {{> schema_item }} + {{~else~}} + .and({{> schema_item }}) + {{~/if~}} + {{~/each~}} +{{~/if~}} + +{{~#if anyOf~}} + z.union([ + {{~#each anyOf~}} + {{~> schema_item ~}}, + {{~/each~}} + ]) +{{~/if~}} + +{{~#if oneOf~}} + z.union([ + {{~#each oneOf~}} + {{~> schema_item ~}}, + {{~/each~}} + ]) +{{~/if~}} + +{{#if (isUnknown .)}} +z.unknown() +{{/if}} + +{{~#*inline "type_array"~}} + {{~#if x-preprocess}} + z.preprocess({{x-preprocess}}, z.array({{~> schema_item items ~}})) + {{else}} + z.array({{~> schema_item items ~}}) + {{~/if~}} + {{~#if minItems}}.min({{minItems}}){{/if~}} + {{~#if maxItems}}.max({{maxItems}}){{/if~}} +{{~/inline~}} + +{{~#*inline "type_boolean"~}} + z.boolean() + {{~#if nullable}}.nullable(){{/if~}} +{{~/inline~}} + +{{~#*inline "type_integer"~}} + {{~#if x-coerce}} + z.coerce.number() + {{~else~}} + z.number() + {{~/if~}} + {{~#if minimum includeZero=true}}.min({{minimum}}){{/if~}} + {{~#if maximum includeZero=true}}.max({{maximum}}){{/if~}} +{{~/inline~}} + +{{~#*inline "type_object"~}} + z.object({ + {{#each properties}} + {{#if description}} + /** + * {{{description}}} + */ + {{/if}} + {{@key}}: {{> schema_item requiredBool=(includes ../required @key)}}, + {{/each}} + }) +{{~/inline~}} + +{{~#*inline "type_string"~}} + {{~#if enum~}} + z.enum([ + {{~#each enum~}} + "{{.}}", + {{~/each~}} + ]) + {{~else~}} + z.string() + {{~#if minLength}}.min({{minLength}}){{/if~}} + {{~#if maxLength}}.max({{maxLength}}){{/if~}} + {{~#if (eq format 'date-time')}}.datetime(){{/if~}} + {{~/if~}} + {{#if transform}}.transform({{{transform}}}){{/if~}} +{{~/inline~}} + diff --git a/x-pack/plugins/security_solution/scripts/openapi/template_service/templates/schemas.handlebars b/x-pack/plugins/security_solution/scripts/openapi/template_service/templates/schemas.handlebars new file mode 100644 index 0000000000000..a6df5d96b124f --- /dev/null +++ b/x-pack/plugins/security_solution/scripts/openapi/template_service/templates/schemas.handlebars @@ -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 { z } from "zod"; + +{{> disclaimer}} + +{{#each importsMap}} +import { + {{#each this}}{{.}},{{/each}} +} from "{{@key}}" +{{/each}} + +{{#each components.schemas}} +{{#description}} +/** + * {{{.}}} + */ +{{/description}} +export type {{@key}} = z.infer; +export const {{@key}} = {{> schema_item}}; + +{{/each}} + +{{#each apiOperations}} +{{#if requestQuery}} +{{#if requestQuery.description}} +/** +* {{{requestQuery.description}}} +*/ +{{/if}} +export type {{operationId}}RequestQuery = z.infer; +export const {{operationId}}RequestQuery = {{> schema_item requestQuery }}; +export type {{operationId}}RequestQueryInput = z.input; +{{/if}} + +{{#if requestParams}} +{{#if requestParams.description}} +/** +* {{{requestParams.description}}} +*/ +{{/if}} +export type {{operationId}}RequestParams = z.infer; +export const {{operationId}}RequestParams = {{> schema_item requestParams }}; +export type {{operationId}}RequestParamsInput = z.input; +{{/if}} + +{{#if requestBody}} +{{#if requestBody.description}} +/** +* {{{requestBody.description}}} +*/ +{{/if}} +export type {{operationId}}RequestBody = z.infer; +export const {{operationId}}RequestBody = {{> schema_item requestBody }}; +export type {{operationId}}RequestBodyInput = z.input; +{{/if}} + +{{#if response}} +{{#if response.description}} +/** +* {{{response.description}}} +*/ +{{/if}} +export type {{operationId}}Response = z.infer; +export const {{operationId}}Response = {{> schema_item response }}; +{{/if}} +{{/each}} diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts index f26531296b6a2..0ff3692971ad4 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.test.ts @@ -109,7 +109,8 @@ describe('ingest_integration tests ', () => { cloud = cloudService.isCloudEnabled, licenseUuid = 'updated-uid', clusterUuid = '', - clusterName = '' + clusterName = '', + isServerlessEnabled = cloudService.isServerlessEnabled ) => ({ type: 'endpoint', enabled: true, @@ -118,7 +119,14 @@ describe('ingest_integration tests ', () => { integration_config: {}, policy: { value: disableProtections( - policyFactory(license, cloud, licenseUuid, clusterUuid, clusterName) + policyFactory( + license, + cloud, + licenseUuid, + clusterUuid, + clusterName, + isServerlessEnabled + ) ), }, artifact_manifest: { value: manifest }, @@ -527,6 +535,7 @@ describe('ingest_integration tests ', () => { beforeEach(() => { licenseEmitter.next(Platinum); // set license level to platinum }); + it('updates successfully when meta fields differ from services', async () => { const mockPolicy = policyFactory(); mockPolicy.meta.cloud = true; // cloud mock will return true @@ -534,6 +543,7 @@ describe('ingest_integration tests ', () => { mockPolicy.meta.cluster_name = 'updated-name'; mockPolicy.meta.cluster_uuid = 'updated-uuid'; mockPolicy.meta.license_uid = 'updated-uid'; + mockPolicy.meta.serverless = false; const logger = loggingSystemMock.create().get('ingest_integration.test'); const callback = getPackagePolicyUpdateCallback( logger, @@ -552,6 +562,7 @@ describe('ingest_integration tests ', () => { policyConfig.inputs[0]!.config!.policy.value.meta.cluster_name = 'original-name'; policyConfig.inputs[0]!.config!.policy.value.meta.cluster_uuid = 'original-uuid'; policyConfig.inputs[0]!.config!.policy.value.meta.license_uid = 'original-uid'; + policyConfig.inputs[0]!.config!.policy.value.meta.serverless = true; const updatedPolicyConfig = await callback( policyConfig, soClient, @@ -569,6 +580,7 @@ describe('ingest_integration tests ', () => { mockPolicy.meta.cluster_name = 'updated-name'; mockPolicy.meta.cluster_uuid = 'updated-uuid'; mockPolicy.meta.license_uid = 'updated-uid'; + mockPolicy.meta.serverless = false; const logger = loggingSystemMock.create().get('ingest_integration.test'); const callback = getPackagePolicyUpdateCallback( logger, @@ -586,6 +598,7 @@ describe('ingest_integration tests ', () => { policyConfig.inputs[0]!.config!.policy.value.meta.cluster_name = 'updated-name'; policyConfig.inputs[0]!.config!.policy.value.meta.cluster_uuid = 'updated-uuid'; policyConfig.inputs[0]!.config!.policy.value.meta.license_uid = 'updated-uid'; + policyConfig.inputs[0]!.config!.policy.value.meta.serverless = false; const updatedPolicyConfig = await callback( policyConfig, soClient, diff --git a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts index 04bc9afa6d3a1..a9da860a5008e 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/fleet_integration.ts @@ -57,14 +57,16 @@ const shouldUpdateMetaValues = ( currentCloudInfo: boolean, currentClusterName: string, currentClusterUUID: string, - currentLicenseUID: string + currentLicenseUID: string, + currentIsServerlessEnabled: boolean ) => { return ( endpointPackagePolicy.meta.license !== currentLicenseType || endpointPackagePolicy.meta.cloud !== currentCloudInfo || endpointPackagePolicy.meta.cluster_name !== currentClusterName || endpointPackagePolicy.meta.cluster_uuid !== currentClusterUUID || - endpointPackagePolicy.meta.license_uid !== currentLicenseUID + endpointPackagePolicy.meta.license_uid !== currentLicenseUID || + endpointPackagePolicy.meta.serverless !== currentIsServerlessEnabled ); }; @@ -221,7 +223,8 @@ export const getPackagePolicyUpdateCallback = ( cloud?.isCloudEnabled, esClientInfo.cluster_name, esClientInfo.cluster_uuid, - licenseService.getLicenseUID() + licenseService.getLicenseUID(), + cloud?.isServerlessEnabled ) ) { newEndpointPackagePolicy.meta.license = licenseService.getLicenseType(); @@ -229,6 +232,7 @@ export const getPackagePolicyUpdateCallback = ( newEndpointPackagePolicy.meta.cluster_name = esClientInfo.cluster_name; newEndpointPackagePolicy.meta.cluster_uuid = esClientInfo.cluster_uuid; newEndpointPackagePolicy.meta.license_uid = licenseService.getLicenseUID(); + newEndpointPackagePolicy.meta.serverless = cloud?.isServerlessEnabled; endpointIntegrationData.inputs[0].config.policy.value = newEndpointPackagePolicy; } diff --git a/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts b/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts index db053fd5c3b0e..75addef37ee6e 100644 --- a/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts +++ b/x-pack/plugins/security_solution/server/fleet_integration/handlers/create_default_policy.ts @@ -49,6 +49,7 @@ export const createDefaultPolicy = ( ? esClientInfo.cluster_uuid : factoryPolicy.meta.cluster_uuid; factoryPolicy.meta.license_uid = licenseService.getLicenseUID(); + factoryPolicy.meta.serverless = cloud.isServerlessEnabled || false; let defaultPolicyPerType: PolicyConfig = config?.type === 'cloud' diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index 6c543fd142522..fbcfe9df91c5a 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -169,5 +169,6 @@ "@kbn/alerts-ui-shared", "@kbn/core-logging-server-mocks", "@kbn/core-lifecycle-browser", + "@kbn/handlebars", ] } diff --git a/x-pack/plugins/security_solution_serverless/kibana.jsonc b/x-pack/plugins/security_solution_serverless/kibana.jsonc index cd2cb7c705634..68b6eb71af8d5 100644 --- a/x-pack/plugins/security_solution_serverless/kibana.jsonc +++ b/x-pack/plugins/security_solution_serverless/kibana.jsonc @@ -19,7 +19,8 @@ "securitySolution", "serverless", "taskManager", - "cloud" + "cloud", + "fleet" ], "optionalPlugins": [ "securitySolutionEss" diff --git a/x-pack/plugins/security_solution_serverless/server/endpoint/services/index.ts b/x-pack/plugins/security_solution_serverless/server/endpoint/services/index.ts index 990731eb640a8..d39a8ed11d24b 100644 --- a/x-pack/plugins/security_solution_serverless/server/endpoint/services/index.ts +++ b/x-pack/plugins/security_solution_serverless/server/endpoint/services/index.ts @@ -6,3 +6,4 @@ */ export { endpointMeteringService } from './metering_service'; +export { setEndpointPackagePolicyServerlessFlag } from './set_package_policy_flag'; diff --git a/x-pack/plugins/security_solution_serverless/server/endpoint/services/metering_service.ts b/x-pack/plugins/security_solution_serverless/server/endpoint/services/metering_service.ts index 2ec94d78722e0..b47450eca235c 100644 --- a/x-pack/plugins/security_solution_serverless/server/endpoint/services/metering_service.ts +++ b/x-pack/plugins/security_solution_serverless/server/endpoint/services/metering_service.ts @@ -113,13 +113,15 @@ export class EndpointMeteringService { creation_timestamp: timestampStr, usage: { type: 'security_solution_endpoint', - sub_type: this.tier, period_seconds: SAMPLE_PERIOD_SECONDS, quantity: 1, }, source: { id: taskId, instance_group_id: projectId, + metadata: { + tier: this.tier, + }, }, }; } diff --git a/x-pack/plugins/security_solution_serverless/server/endpoint/services/set_package_policy_flag.test.ts b/x-pack/plugins/security_solution_serverless/server/endpoint/services/set_package_policy_flag.test.ts new file mode 100644 index 0000000000000..54a95ae68a1b7 --- /dev/null +++ b/x-pack/plugins/security_solution_serverless/server/endpoint/services/set_package_policy_flag.test.ts @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { cloneDeep } from 'lodash'; + +import type { SavedObjectsClientContract } from '@kbn/core/server'; +import { elasticsearchServiceMock, savedObjectsClientMock } from '@kbn/core/server/mocks'; +import type { ElasticsearchClientMock } from '@kbn/core/server/mocks'; +import { + FLEET_ENDPOINT_PACKAGE, + PACKAGE_POLICY_SAVED_OBJECT_TYPE, + SO_SEARCH_LIMIT, +} from '@kbn/fleet-plugin/common'; +import type { PackagePolicy } from '@kbn/fleet-plugin/common'; +import type { PackagePolicyClient } from '@kbn/fleet-plugin/server'; +import { createPackagePolicyServiceMock } from '@kbn/fleet-plugin/server/mocks'; +import { policyFactory } from '@kbn/security-solution-plugin/common/endpoint/models/policy_config'; + +import { setEndpointPackagePolicyServerlessFlag } from './set_package_policy_flag'; + +describe('setEndpointPackagePolicyServerlessFlag', () => { + let esClientMock: ElasticsearchClientMock; + let soClientMock: jest.Mocked; + let packagePolicyServiceMock: jest.Mocked; + + function generatePackagePolicy(policy = policyFactory()): PackagePolicy { + return { + inputs: [ + { + config: { + policy: { + value: policy, + }, + }, + }, + ], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any as PackagePolicy; + } + + beforeEach(() => { + esClientMock = elasticsearchServiceMock.createClusterClient().asInternalUser; + soClientMock = savedObjectsClientMock.create(); + packagePolicyServiceMock = createPackagePolicyServiceMock(); + }); + + it('updates serverless flag for endpoint policies', async () => { + const packagePolicy1 = generatePackagePolicy(); + const packagePolicy2 = generatePackagePolicy(); + packagePolicyServiceMock.list.mockResolvedValue({ + items: [packagePolicy1, packagePolicy2], + page: 1, + perPage: SO_SEARCH_LIMIT, + total: 2, + }); + packagePolicyServiceMock.bulkCreate.mockImplementation(); + + await setEndpointPackagePolicyServerlessFlag( + soClientMock, + esClientMock, + packagePolicyServiceMock + ); + + const expectedPolicy1 = cloneDeep(packagePolicy1); + expectedPolicy1!.inputs[0]!.config!.policy.value.meta.serverless = true; + const expectedPolicy2 = cloneDeep(packagePolicy2); + expectedPolicy2!.inputs[0]!.config!.policy.value.meta.serverless = true; + const expectedPolicies = [expectedPolicy1, expectedPolicy2]; + expect(packagePolicyServiceMock.list).toBeCalledWith(soClientMock, { + page: 1, + perPage: SO_SEARCH_LIMIT, + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${FLEET_ENDPOINT_PACKAGE}`, + }); + expect(packagePolicyServiceMock.bulkUpdate).toBeCalledWith( + soClientMock, + esClientMock, + expectedPolicies + ); + }); + + it('batches properly when over perPage', async () => { + packagePolicyServiceMock.list + .mockResolvedValueOnce({ + items: [], + page: 1, + perPage: SO_SEARCH_LIMIT, + total: SO_SEARCH_LIMIT, + }) + .mockResolvedValueOnce({ + items: [], + page: 2, + perPage: SO_SEARCH_LIMIT, + total: 1, + }); + packagePolicyServiceMock.bulkCreate.mockImplementation(); + + await setEndpointPackagePolicyServerlessFlag( + soClientMock, + esClientMock, + packagePolicyServiceMock + ); + + expect(packagePolicyServiceMock.list).toHaveBeenNthCalledWith(1, soClientMock, { + page: 1, + perPage: SO_SEARCH_LIMIT, + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${FLEET_ENDPOINT_PACKAGE}`, + }); + expect(packagePolicyServiceMock.list).toHaveBeenNthCalledWith(2, soClientMock, { + page: 2, + perPage: SO_SEARCH_LIMIT, + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${FLEET_ENDPOINT_PACKAGE}`, + }); + }); +}); diff --git a/x-pack/plugins/security_solution_serverless/server/endpoint/services/set_package_policy_flag.ts b/x-pack/plugins/security_solution_serverless/server/endpoint/services/set_package_policy_flag.ts new file mode 100644 index 0000000000000..0c6191e8df706 --- /dev/null +++ b/x-pack/plugins/security_solution_serverless/server/endpoint/services/set_package_policy_flag.ts @@ -0,0 +1,89 @@ +/* + * 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 { SavedObjectsClientContract, ElasticsearchClient } from '@kbn/core/server'; +import type { PackagePolicyClient } from '@kbn/fleet-plugin/server'; +import type { ListResult, PackagePolicy } from '@kbn/fleet-plugin/common'; +import { + FLEET_ENDPOINT_PACKAGE, + PACKAGE_POLICY_SAVED_OBJECT_TYPE, + SO_SEARCH_LIMIT, +} from '@kbn/fleet-plugin/common'; + +// set all endpoint policies serverless flag to true +// required so that endpoint will write heartbeats +export async function setEndpointPackagePolicyServerlessFlag( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + packagePolicyService: PackagePolicyClient +): Promise { + const perPage: number = SO_SEARCH_LIMIT; + let page: number = 1; + let endpointPackagesResult: ListResult | undefined; + + while (page === 1 || endpointPackagesResult?.total === perPage) { + endpointPackagesResult = await getEndpointPackagePolicyBatch( + soClient, + packagePolicyService, + page, + perPage + ); + await processBatch(endpointPackagesResult, soClient, esClient, packagePolicyService); + page++; + } +} + +function getEndpointPackagePolicyBatch( + soClient: SavedObjectsClientContract, + packagePolicyService: PackagePolicyClient, + page: number, + perPage: number +): Promise> { + return packagePolicyService.list(soClient, { + page, + perPage, + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${FLEET_ENDPOINT_PACKAGE}`, + }); +} + +async function processBatch( + endpointPackagesResult: ListResult, + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + packagePolicyService: PackagePolicyClient +): Promise { + if (!endpointPackagesResult.total) { + return; + } + + const updatedEndpointPackages = endpointPackagesResult.items.map((endpointPackage) => ({ + ...endpointPackage, + inputs: endpointPackage.inputs.map((input) => { + const config = input?.config || {}; + const policy = config.policy || {}; + const policyValue = policy?.value || {}; + const meta = policyValue?.meta || {}; + return { + ...input, + config: { + ...config, + policy: { + ...policy, + value: { + ...policyValue, + meta: { + ...meta, + serverless: true, + }, + }, + }, + }, + }; + }), + })); + await packagePolicyService.bulkUpdate(soClient, esClient, updatedEndpointPackages); +} diff --git a/x-pack/plugins/security_solution_serverless/server/plugin.ts b/x-pack/plugins/security_solution_serverless/server/plugin.ts index 4ecf5196cbd61..cef36b0dab9db 100644 --- a/x-pack/plugins/security_solution_serverless/server/plugin.ts +++ b/x-pack/plugins/security_solution_serverless/server/plugin.ts @@ -12,9 +12,8 @@ import type { CoreStart, Logger, } from '@kbn/core/server'; + import { getProductAppFeatures } from '../common/pli/pli_features'; -import { METERING_TASK as ENDPOINT_METERING_TASK } from './endpoint/constants/metering'; -import { endpointMeteringService } from './endpoint/services'; import type { ServerlessSecurityConfig } from './config'; import type { @@ -25,6 +24,11 @@ import type { } from './types'; import { SecurityUsageReportingTask } from './task_manager/usage_reporting_task'; import { cloudSecurityMetringTaskProperties } from './cloud_security/cloud_security_metering_task_config'; +import { METERING_TASK as ENDPOINT_METERING_TASK } from './endpoint/constants/metering'; +import { + endpointMeteringService, + setEndpointPackagePolicyServerlessFlag, +} from './endpoint/services'; export class SecuritySolutionServerlessPlugin implements @@ -92,6 +96,9 @@ export class SecuritySolutionServerlessPlugin } public start(_coreStart: CoreStart, pluginsSetup: SecuritySolutionServerlessPluginStartDeps) { + const internalESClient = _coreStart.elasticsearch.client.asInternalUser; + const internalSOClient = _coreStart.savedObjects.createInternalRepository(); + this.cspmUsageReportingTask?.start({ taskManager: pluginsSetup.taskManager, interval: cloudSecurityMetringTaskProperties.interval, @@ -101,6 +108,12 @@ export class SecuritySolutionServerlessPlugin taskManager: pluginsSetup.taskManager, interval: ENDPOINT_METERING_TASK.INTERVAL, }); + + setEndpointPackagePolicyServerlessFlag( + internalSOClient, + internalESClient, + pluginsSetup.fleet.packagePolicyService + ); return {}; } diff --git a/x-pack/plugins/security_solution_serverless/server/types.ts b/x-pack/plugins/security_solution_serverless/server/types.ts index ac45cadd21c6d..5e6ab2fbb5a1b 100644 --- a/x-pack/plugins/security_solution_serverless/server/types.ts +++ b/x-pack/plugins/security_solution_serverless/server/types.ts @@ -18,6 +18,9 @@ import type { import type { CloudSetup } from '@kbn/cloud-plugin/server'; import type { SecuritySolutionEssPluginSetup } from '@kbn/security-solution-ess/server'; import type { MlPluginSetup } from '@kbn/ml-plugin/server'; +import type { FleetStartContract } from '@kbn/fleet-plugin/server'; + +import type { ProductTier } from '../common/product'; import type { ServerlessSecurityConfig } from './config'; @@ -41,6 +44,7 @@ export interface SecuritySolutionServerlessPluginStartDeps { securitySolution: SecuritySolutionPluginStart; features: PluginStartContract; taskManager: TaskManagerStartContract; + fleet: FleetStartContract; } export interface UsageRecord { @@ -63,6 +67,11 @@ export interface UsageMetrics { export interface UsageSource { id: string; instance_group_id: string; + metadata?: UsageSourceMetadata; +} + +export interface UsageSourceMetadata { + tier?: ProductTier; } export interface SecurityUsageReportingTaskSetupContract { diff --git a/x-pack/plugins/security_solution_serverless/tsconfig.json b/x-pack/plugins/security_solution_serverless/tsconfig.json index b69dbcb5b189a..f8e322f580837 100644 --- a/x-pack/plugins/security_solution_serverless/tsconfig.json +++ b/x-pack/plugins/security_solution_serverless/tsconfig.json @@ -35,6 +35,7 @@ "@kbn/kibana-utils-plugin", "@kbn/task-manager-plugin", "@kbn/cloud-plugin", - "@kbn/cloud-security-posture-plugin" + "@kbn/cloud-security-posture-plugin", + "@kbn/fleet-plugin" ] } diff --git a/x-pack/plugins/spaces/kibana.jsonc b/x-pack/plugins/spaces/kibana.jsonc index 7a033956a8f45..efa3f85cd5b77 100644 --- a/x-pack/plugins/spaces/kibana.jsonc +++ b/x-pack/plugins/spaces/kibana.jsonc @@ -16,7 +16,6 @@ "licensing" ], "optionalPlugins": [ - "advancedSettings", "home", "management", "usageCollection" @@ -29,4 +28,4 @@ "common" ] } -} +} \ No newline at end of file diff --git a/x-pack/plugins/spaces/public/advanced_settings/advanced_settings_service.test.tsx b/x-pack/plugins/spaces/public/advanced_settings/advanced_settings_service.test.tsx deleted file mode 100644 index 6b547bfc9bbf4..0000000000000 --- a/x-pack/plugins/spaces/public/advanced_settings/advanced_settings_service.test.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { advancedSettingsMock } from '@kbn/advanced-settings-plugin/public/mocks'; - -import { AdvancedSettingsService } from './advanced_settings_service'; - -const componentRegistryMock = advancedSettingsMock.createSetupContract(); - -describe('Advanced Settings Service', () => { - describe('#setup', () => { - it('registers space-aware components to augment the advanced settings screen', () => { - const deps = { - getActiveSpace: jest.fn().mockResolvedValue({ id: 'foo', name: 'foo-space' }), - componentRegistry: componentRegistryMock.component, - }; - - const advancedSettingsService = new AdvancedSettingsService(); - advancedSettingsService.setup(deps); - - expect(deps.componentRegistry.register).toHaveBeenCalledTimes(2); - expect(deps.componentRegistry.register).toHaveBeenCalledWith( - componentRegistryMock.component.componentType.PAGE_TITLE_COMPONENT, - expect.any(Function), - true - ); - - expect(deps.componentRegistry.register).toHaveBeenCalledWith( - componentRegistryMock.component.componentType.PAGE_SUBTITLE_COMPONENT, - expect.any(Function), - true - ); - }); - }); -}); diff --git a/x-pack/plugins/spaces/public/advanced_settings/advanced_settings_service.tsx b/x-pack/plugins/spaces/public/advanced_settings/advanced_settings_service.tsx deleted file mode 100644 index e068b8e8d38be..0000000000000 --- a/x-pack/plugins/spaces/public/advanced_settings/advanced_settings_service.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import type { AdvancedSettingsSetup } from '@kbn/advanced-settings-plugin/public'; - -import { AdvancedSettingsSubtitle, AdvancedSettingsTitle } from './components'; -import type { Space } from '../../common'; - -interface SetupDeps { - getActiveSpace: () => Promise; - componentRegistry: AdvancedSettingsSetup['component']; -} - -export class AdvancedSettingsService { - public setup({ getActiveSpace, componentRegistry }: SetupDeps) { - const PageTitle = () => ; - const SubTitle = () => ; - - componentRegistry.register( - componentRegistry.componentType.PAGE_TITLE_COMPONENT, - PageTitle, - true - ); - componentRegistry.register( - componentRegistry.componentType.PAGE_SUBTITLE_COMPONENT, - SubTitle, - true - ); - } -} diff --git a/x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_subtitle/advanced_settings_subtitle.test.tsx b/x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_subtitle/advanced_settings_subtitle.test.tsx deleted file mode 100644 index 7352e769f7e55..0000000000000 --- a/x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_subtitle/advanced_settings_subtitle.test.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiCallOut } from '@elastic/eui'; -import { act } from '@testing-library/react'; -import React from 'react'; - -import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers'; - -import { AdvancedSettingsSubtitle } from './advanced_settings_subtitle'; - -describe('AdvancedSettingsSubtitle', () => { - it('renders as expected', async () => { - const space = { - id: 'my-space', - name: 'My Space', - disabledFeatures: [], - }; - - const wrapper = mountWithIntl( - Promise.resolve(space)} /> - ); - - // Wait for active space to resolve before requesting the component to update - await act(async () => { - await nextTick(); - wrapper.update(); - }); - - expect(wrapper.find(EuiCallOut)).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_subtitle/advanced_settings_subtitle.tsx b/x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_subtitle/advanced_settings_subtitle.tsx deleted file mode 100644 index 61c7f063b3dae..0000000000000 --- a/x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_subtitle/advanced_settings_subtitle.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiCallOut, EuiSpacer } from '@elastic/eui'; -import React, { Fragment, useEffect, useState } from 'react'; - -import { FormattedMessage } from '@kbn/i18n-react'; - -import type { Space } from '../../../../common'; - -interface Props { - getActiveSpace: () => Promise; -} - -export const AdvancedSettingsSubtitle = (props: Props) => { - const [activeSpace, setActiveSpace] = useState(null); - - useEffect(() => { - props.getActiveSpace().then((space) => setActiveSpace(space)); - }, [props]); - - if (!activeSpace) return null; - - return ( - - - {activeSpace.name}, - }} - /> - } - /> - - ); -}; diff --git a/x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_title/advanced_settings_title.test.tsx b/x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_title/advanced_settings_title.test.tsx deleted file mode 100644 index 63f340863e859..0000000000000 --- a/x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_title/advanced_settings_title.test.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { act } from '@testing-library/react'; -import React from 'react'; - -import { mountWithIntl } from '@kbn/test-jest-helpers'; - -import { AdvancedSettingsTitle } from './advanced_settings_title'; -import { SpaceAvatarInternal } from '../../../space_avatar/space_avatar_internal'; - -describe('AdvancedSettingsTitle', () => { - it('renders without crashing', async () => { - const space = { - id: 'my-space', - name: 'My Space', - disabledFeatures: [], - }; - - const wrapper = mountWithIntl( - Promise.resolve(space)} /> - ); - - await act(async () => {}); - - // wait for SpaceAvatar to lazy-load - await act(async () => {}); - wrapper.update(); - - expect(wrapper.find(SpaceAvatarInternal)).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_title/advanced_settings_title.tsx b/x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_title/advanced_settings_title.tsx deleted file mode 100644 index 5ee988969d969..0000000000000 --- a/x-pack/plugins/spaces/public/advanced_settings/components/advanced_settings_title/advanced_settings_title.tsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiTitle } from '@elastic/eui'; -import React, { lazy, Suspense, useEffect, useState } from 'react'; - -import { FormattedMessage } from '@kbn/i18n-react'; - -import type { Space } from '../../../../common'; -import { getSpaceAvatarComponent } from '../../../space_avatar'; - -// No need to wrap LazySpaceAvatar in an error boundary, because it is one of the first chunks loaded when opening Kibana. -const LazySpaceAvatar = lazy(() => - getSpaceAvatarComponent().then((component) => ({ default: component })) -); - -interface Props { - getActiveSpace: () => Promise; -} - -export const AdvancedSettingsTitle = (props: Props) => { - const [activeSpace, setActiveSpace] = useState(null); - - useEffect(() => { - props.getActiveSpace().then((space) => setActiveSpace(space)); - }, [props]); - - if (!activeSpace) return null; - - return ( - - - }> - - - - - -

- -

-
-
-
- ); -}; diff --git a/x-pack/plugins/spaces/public/plugin.test.ts b/x-pack/plugins/spaces/public/plugin.test.ts index f527a77d48bc3..91e1a21752959 100644 --- a/x-pack/plugins/spaces/public/plugin.test.ts +++ b/x-pack/plugins/spaces/public/plugin.test.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { advancedSettingsMock } from '@kbn/advanced-settings-plugin/public/mocks'; import { coreMock } from '@kbn/core/public/mocks'; import { homePluginMock } from '@kbn/home-plugin/public/mocks'; import { @@ -63,29 +62,6 @@ describe('Spaces plugin', () => { }) ); }); - - it('should register the advanced settings components if the advanced_settings plugin is available', () => { - const coreSetup = coreMock.createSetup(); - const advancedSettings = advancedSettingsMock.createSetupContract(); - - const plugin = new SpacesPlugin(coreMock.createPluginInitializerContext()); - plugin.setup(coreSetup, { advancedSettings }); - - expect(advancedSettings.component.register.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - "advanced_settings_page_title", - [Function], - true, - ], - Array [ - "advanced_settings_page_subtitle", - [Function], - true, - ], - ] - `); - }); }); describe('#start', () => { diff --git a/x-pack/plugins/spaces/public/plugin.tsx b/x-pack/plugins/spaces/public/plugin.tsx index 0546f0d4e32e5..33cbcc3a47227 100644 --- a/x-pack/plugins/spaces/public/plugin.tsx +++ b/x-pack/plugins/spaces/public/plugin.tsx @@ -5,13 +5,11 @@ * 2.0. */ -import type { AdvancedSettingsSetup } from '@kbn/advanced-settings-plugin/public'; import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public'; import type { FeaturesPluginStart } from '@kbn/features-plugin/public'; import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; import type { ManagementSetup, ManagementStart } from '@kbn/management-plugin/public'; -import { AdvancedSettingsService } from './advanced_settings'; import type { ConfigType } from './config'; import { createSpacesFeatureCatalogueEntry } from './create_feature_catalogue_entry'; import { ManagementService } from './management'; @@ -22,7 +20,6 @@ import type { SpacesApi } from './types'; import { getUiApi } from './ui_api'; export interface PluginsSetup { - advancedSettings?: AdvancedSettingsSetup; home?: HomePublicPluginSetup; management?: ManagementSetup; } @@ -78,14 +75,6 @@ export class SpacesPlugin implements Plugin this.spacesManager.getActiveSpace(), - componentRegistry: plugins.advancedSettings.component, - }); - } - spaceSelectorApp.create({ getStartServices: core.getStartServices, application: core.application, diff --git a/x-pack/plugins/spaces/tsconfig.json b/x-pack/plugins/spaces/tsconfig.json index fd3312087ebbb..43ea0f1c6c562 100644 --- a/x-pack/plugins/spaces/tsconfig.json +++ b/x-pack/plugins/spaces/tsconfig.json @@ -8,7 +8,6 @@ "@kbn/features-plugin", "@kbn/licensing-plugin", "@kbn/es-ui-shared-plugin", - "@kbn/advanced-settings-plugin", "@kbn/home-plugin", "@kbn/kibana-react-plugin", "@kbn/management-plugin", diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/formatter.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/formatter.test.tsx index 0464c36ba2429..3b6a815771c73 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/formatter.test.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/formatter.test.tsx @@ -96,6 +96,19 @@ describe('format', () => { }; }); + it('leaves un-nested fields as is', () => { + const projectSourceContent = 'UUUUUUUIJLVIK'; + formValues['source.project.content'] = projectSourceContent; + formValues['ssl.verification_mode'] = 'full'; + formValues.type = 'browser'; + expect(format(formValues)).toEqual( + expect.objectContaining({ + ['source.project.content']: projectSourceContent, + ['ssl.verification_mode']: 'full', + }) + ); + }); + it.each([[true], [false]])('correctly formats form fields to monitor type', (enabled) => { formValues.enabled = enabled; expect(format(formValues)).toEqual({ diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/formatter.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/formatter.ts index e27fb0b908ee7..62f0919cb6875 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/formatter.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_add_edit/form/formatter.ts @@ -15,7 +15,7 @@ export const serializeNestedFormField = (fields: Record) => { Object.keys(defaults).map((key) => { /* split key names on dot to handle dot notation fields, * which are changed to nested fields by react-hook-form */ - monitorFields[key] = get(fields, key.split('.')) ?? defaults[key as ConfigKey]; + monitorFields[key] = get(fields, key.split('.')) ?? fields[key] ?? defaults[key as ConfigKey]; }); return monitorFields as MonitorFields; }; diff --git a/x-pack/plugins/synthetics/server/routes/synthetics_service/run_once_monitor.ts b/x-pack/plugins/synthetics/server/routes/synthetics_service/run_once_monitor.ts index be57e38b7978b..e1ddceb4e5b7c 100644 --- a/x-pack/plugins/synthetics/server/routes/synthetics_service/run_once_monitor.ts +++ b/x-pack/plugins/synthetics/server/routes/synthetics_service/run_once_monitor.ts @@ -9,7 +9,7 @@ import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; import { PrivateLocationAttributes } from '../../runtime_types/private_locations'; import { getPrivateLocationsForMonitor } from '../monitor_cruds/add_monitor'; import { SyntheticsRestApiRouteFactory } from '../types'; -import { MonitorFields } from '../../../common/runtime_types'; +import { ConfigKey, MonitorFields } from '../../../common/runtime_types'; import { SYNTHETICS_API_URLS } from '../../../common/constants'; import { validateMonitor } from '../monitor_cruds/monitor_validation'; @@ -36,19 +36,24 @@ export const runOnceSyntheticsMonitorRoute: SyntheticsRestApiRouteFactory = () = const spaceId = server.spaces?.spacesService.getSpaceId(request) ?? DEFAULT_SPACE_ID; - if (!validationResult.valid || !validationResult.decodedMonitor) { + const decodedMonitor = validationResult.decodedMonitor; + if (!validationResult.valid || !decodedMonitor) { const { reason: message, details, payload } = validationResult; return response.badRequest({ body: { message, attributes: { details, ...payload } } }); } const privateLocations: PrivateLocationAttributes[] = await getPrivateLocationsForMonitor( savedObjectsClient, - validationResult.decodedMonitor + decodedMonitor ); const [, errors] = await syntheticsMonitorClient.testNowConfigs( { - monitor: { ...validationResult.decodedMonitor, config_id: monitorId } as MonitorFields, + monitor: { + ...decodedMonitor, + [ConfigKey.CONFIG_ID]: monitorId, + [ConfigKey.MONITOR_QUERY_ID]: monitorId, + } as MonitorFields, id: monitorId, testRunId: monitorId, }, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 720b0e8d549a2..be49724e18c72 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -130,7 +130,6 @@ "advancedSettings.globalCalloutSubtitle": "Les modifications seront appliquées à tous les utilisateurs dans l'ensemble des espaces. Cela inclut les utilisateurs Kibana natifs et les utilisateurs qui se connectent via l'authentification unique.", "advancedSettings.globalCalloutTitle": "Les modifications auront une incidence sur tous les paramètres utilisateur dans l'ensemble des espaces", "advancedSettings.globalSettingsTabTitle": "Paramètres généraux", - "advancedSettings.pageTitle": "Paramètres", "advancedSettings.searchBar.unableToParseQueryErrorMessage": "Impossible d'analyser la requête", "advancedSettings.searchBarAriaLabel": "Rechercher dans les paramètres avancés", "advancedSettings.spaceSettingsTabTitle": "Paramètres de l'espace", @@ -409,11 +408,6 @@ "controls.optionsList.popover.invalidSelectionsSectionTitle": "{invalidSelectionCount, plural, one {Sélection ignorée} many {Sélections ignorées} other {Sélections ignorées}}", "controls.optionsList.popover.suggestionsAriaLabel": "{optionCount, plural, one {option disponible} many {options disponibles} other {options disponibles}} pour {fieldName}", "controls.rangeSlider.errors.fieldNotFound": "Impossible de localiser le champ : {fieldName}", - "controls.controlGroup.emptyState.addControlButtonTitle": "Ajouter un contrôle", - "controls.controlGroup.emptyState.badgeText": "Nouveauté", - "controls.controlGroup.emptyState.callToAction": "Le filtrage des données s'est amélioré grâce aux contrôles, qui vous permettent d'afficher uniquement les données que vous souhaitez explorer.", - "controls.controlGroup.emptyState.dismissButton": "Rejeter", - "controls.controlGroup.emptyState.twoLineLoadingTitle": "...", "controls.controlGroup.floatingActions.clearTitle": "Effacer", "controls.controlGroup.floatingActions.editTitle": "Modifier", "controls.controlGroup.floatingActions.removeTitle": "Supprimer", @@ -31155,7 +31149,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.threatMatchTitle": "Correspondance d'indicateur", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.thresholdTypeDescription": "Agrégez les résultats de recherche pour détecter à quel moment le nombre de correspondances dépasse le seuil.", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.thresholdTypeTitle": "Seuil", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.savedQueryFieldRequiredError": "Impossible de charger la requête enregistrée. Sélectionnez-en une nouvelle ou ajoutez une requête personnalisée.", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.SavedQueryFormRowLabel": "Requête enregistrée", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.source": "Source", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.suppressionMissingFieldsLabel": "Si un champ de suppression est manquant", @@ -35647,7 +35640,6 @@ "xpack.spaces.legacyUrlConflict.calloutBodyText": "Assurez-vous qu'il s'agit du {objectNoun} que vous recherchez. Sinon, consultez l'autre. {documentationLink}", "xpack.spaces.legacyUrlConflict.linkButton": "Accéder à un autre {objectNoun}", "xpack.spaces.legacyURLConflict.toolTipText": "Ce {objectNoun} possède l'ID [id={currentObjectId}]. L'autre {objectNoun} possède l'ID [id={otherObjectId}].", - "xpack.spaces.management.advancedSettingsSubtitle.applyingSettingsOnPageToSpaceDescription": "Les paramètres de cette page s'appliquent à l'espace {spaceName}, sauf indication contraire.", "xpack.spaces.management.confirmDeleteModal.confirmButton": "{isLoading, select, true {Suppression de l'espace et de tous les contenus…} other {Supprimer l'espace et tous les contenus}}", "xpack.spaces.management.confirmDeleteModal.description": "Cet espace et {allContents} seront définitivement supprimés.", "xpack.spaces.management.copyToSpace.copyStatusSummary.conflictsMessage": "Conflits détectés dans l'espace {space}. Développez cette section pour les résoudre.", @@ -35691,7 +35683,6 @@ "xpack.spaces.legacyUrlConflict.calloutTitle": "2 objets enregistrés utilisent cette URL", "xpack.spaces.legacyUrlConflict.dismissButton": "Rejeter", "xpack.spaces.legacyUrlConflict.documentationLinkText": "En savoir plus", - "xpack.spaces.management.advancedSettingsTitle.settingsTitle": "Paramètres", "xpack.spaces.management.confirmAlterActiveSpaceModal.cancelButton": "Annuler", "xpack.spaces.management.confirmAlterActiveSpaceModal.reloadWarningMessage": "Vous avez mis à jour les fonctionnalités visibles dans cet espace. Votre page sera rechargée après l'enregistrement.", "xpack.spaces.management.confirmAlterActiveSpaceModal.title": "Confirmer la mise à jour de l'espace", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a239b3d1e0477..69d028fc9b788 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -130,7 +130,6 @@ "advancedSettings.globalCalloutSubtitle": "変更はすべてのスペースのすべてのユーザーに適用されます。これにはネイティブKibanaユーザーとシングルサインオンユーザーの両方が含まれます。", "advancedSettings.globalCalloutTitle": "変更はすべてのスペースのすべてのユーザー設定に影響します", "advancedSettings.globalSettingsTabTitle": "グローバル設定", - "advancedSettings.pageTitle": "設定", "advancedSettings.searchBar.unableToParseQueryErrorMessage": "クエリをパースできません", "advancedSettings.searchBarAriaLabel": "高度な設定を検索", "advancedSettings.spaceSettingsTabTitle": "スペース設定", @@ -409,11 +408,6 @@ "controls.optionsList.popover.invalidSelectionsSectionTitle": "{invalidSelectionCount, plural, other {選択項目}}が無視されました", "controls.optionsList.popover.suggestionsAriaLabel": "{fieldName}の{optionCount, plural, other {オプション}}があります", "controls.rangeSlider.errors.fieldNotFound": "フィールドが見つかりませんでした:{fieldName}", - "controls.controlGroup.emptyState.addControlButtonTitle": "コントロールを追加", - "controls.controlGroup.emptyState.badgeText": "新規", - "controls.controlGroup.emptyState.callToAction": "データのフィルタリングはコントロールによって効果的になりました。探索するデータのみを表示できます。", - "controls.controlGroup.emptyState.dismissButton": "閉じる", - "controls.controlGroup.emptyState.twoLineLoadingTitle": "...", "controls.controlGroup.floatingActions.clearTitle": "クリア", "controls.controlGroup.floatingActions.editTitle": "編集", "controls.controlGroup.floatingActions.removeTitle": "削除", @@ -31154,7 +31148,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.threatMatchTitle": "インジケーター一致", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.thresholdTypeDescription": "クエリ結果を集約し、いつ一致数がしきい値を超えるのかを検出します。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.thresholdTypeTitle": "しきい値", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.savedQueryFieldRequiredError": "保存されたクエリを読み込めませんでした。新しいクエリを選択するか、カスタムクエリを追加してください。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.SavedQueryFormRowLabel": "保存されたクエリ", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.source": "送信元", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.suppressionMissingFieldsLabel": "抑制フィールドが欠落している場合", @@ -35646,7 +35639,6 @@ "xpack.spaces.legacyUrlConflict.calloutBodyText": "これが検索している{objectNoun}であることを確認してください。そうでない場合は、他の項目に移動します。{documentationLink}", "xpack.spaces.legacyUrlConflict.linkButton": "他の{objectNoun}に移動", "xpack.spaces.legacyURLConflict.toolTipText": "この{objectNoun}は[id={currentObjectId}]があります。他のは{objectNoun}[id={otherObjectId}]があります。", - "xpack.spaces.management.advancedSettingsSubtitle.applyingSettingsOnPageToSpaceDescription": "このページの設定は、別途指定されていない限り{spaceName}スペースに適用されます。", "xpack.spaces.management.confirmDeleteModal.confirmButton": "{isLoading, select, true {スペースとすべてのコンテンツを削除中...} other {スペースとすべてのコンテンツを削除}}", "xpack.spaces.management.confirmDeleteModal.description": "このスペースと{allContents}は完全に削除されます。", "xpack.spaces.management.copyToSpace.copyStatusSummary.conflictsMessage": "{space}スペースで競合が検出されました。解決するにはこのセクションを拡張してください。", @@ -35690,7 +35682,6 @@ "xpack.spaces.legacyUrlConflict.calloutTitle": "2つの保存されたオブジェクトがこのURLを使用します", "xpack.spaces.legacyUrlConflict.dismissButton": "閉じる", "xpack.spaces.legacyUrlConflict.documentationLinkText": "詳細", - "xpack.spaces.management.advancedSettingsTitle.settingsTitle": "設定", "xpack.spaces.management.confirmAlterActiveSpaceModal.cancelButton": "キャンセル", "xpack.spaces.management.confirmAlterActiveSpaceModal.reloadWarningMessage": "このスペースで表示される機能を更新しました。保存後にページが更新されます。", "xpack.spaces.management.confirmAlterActiveSpaceModal.title": "スペースの更新の確認", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index fa22bef215154..293e0f0fcd4b6 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -130,7 +130,6 @@ "advancedSettings.globalCalloutSubtitle": "将对所有工作区的所有用户应用更改。这包括本机 Kibana 用户和单点登录用户。", "advancedSettings.globalCalloutTitle": "更改将影响所有工作区的所有用户设置", "advancedSettings.globalSettingsTabTitle": "常规设置", - "advancedSettings.pageTitle": "设置", "advancedSettings.searchBar.unableToParseQueryErrorMessage": "无法解析查询", "advancedSettings.searchBarAriaLabel": "搜索高级设置", "advancedSettings.spaceSettingsTabTitle": "工作区设置", @@ -409,11 +408,6 @@ "controls.optionsList.popover.invalidSelectionsSectionTitle": "已忽略{invalidSelectionCount, plural, other {选择的内容}}", "controls.optionsList.popover.suggestionsAriaLabel": "{fieldName} 的可用{optionCount, plural, other {选项}}", "controls.rangeSlider.errors.fieldNotFound": "找不到字段:{fieldName}", - "controls.controlGroup.emptyState.addControlButtonTitle": "添加控件", - "controls.controlGroup.emptyState.badgeText": "新建", - "controls.controlGroup.emptyState.callToAction": "使用控件可以更有效地筛选数据,允许您仅显示要浏览的数据。", - "controls.controlGroup.emptyState.dismissButton": "关闭", - "controls.controlGroup.emptyState.twoLineLoadingTitle": "...", "controls.controlGroup.floatingActions.clearTitle": "清除", "controls.controlGroup.floatingActions.editTitle": "编辑", "controls.controlGroup.floatingActions.removeTitle": "删除", @@ -31150,7 +31144,6 @@ "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.threatMatchTitle": "指标匹配", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.thresholdTypeDescription": "聚合查询结果以检测匹配数目何时超过阈值。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.ruleTypeField.thresholdTypeTitle": "阈值", - "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.savedQueryFieldRequiredError": "无法加载已保存查询。选择新查询或添加定制查询。", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.SavedQueryFormRowLabel": "已保存查询", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.source": "源", "xpack.securitySolution.detectionEngine.createRule.stepDefineRule.suppressionMissingFieldsLabel": "如果阻止字段缺失", @@ -35640,7 +35633,6 @@ "xpack.spaces.legacyUrlConflict.calloutBodyText": "检查这是否是您正寻找的 {objectNoun}。否则,请前往另一个。{documentationLink}", "xpack.spaces.legacyUrlConflict.linkButton": "前往其他 {objectNoun}", "xpack.spaces.legacyURLConflict.toolTipText": "此 {objectNoun} 具有 [id={currentObjectId}]。其他 {objectNoun} 具有 [id={otherObjectId}]。", - "xpack.spaces.management.advancedSettingsSubtitle.applyingSettingsOnPageToSpaceDescription": "除非已指定,否则此页面上的设置适用于 {spaceName} 空间。", "xpack.spaces.management.confirmDeleteModal.confirmButton": "{isLoading, select, true {正在删除工作区和所有内容……} other {删除工作区和所有内容}}", "xpack.spaces.management.confirmDeleteModal.description": "此工作区和{allContents}将被永久删除。", "xpack.spaces.management.copyToSpace.copyStatusSummary.conflictsMessage": "在 {space} 工作区中检测到冲突。展开此部分可进行解决。", @@ -35684,7 +35676,6 @@ "xpack.spaces.legacyUrlConflict.calloutTitle": "2 个已保存对象使用此 URL", "xpack.spaces.legacyUrlConflict.dismissButton": "关闭", "xpack.spaces.legacyUrlConflict.documentationLinkText": "了解详情", - "xpack.spaces.management.advancedSettingsTitle.settingsTitle": "设置", "xpack.spaces.management.confirmAlterActiveSpaceModal.cancelButton": "取消", "xpack.spaces.management.confirmAlterActiveSpaceModal.reloadWarningMessage": "您已更新此工作区中的可见功能。保存后,您的页面将重新加载。", "xpack.spaces.management.confirmAlterActiveSpaceModal.title": "确认更新工作区", diff --git a/x-pack/test/functional/es_archives/reporting/bwc/6_2/data.json.gz b/x-pack/test/functional/es_archives/reporting/bwc/6_2/data.json.gz index 85949bd32006e..18a22390afd9b 100644 Binary files a/x-pack/test/functional/es_archives/reporting/bwc/6_2/data.json.gz and b/x-pack/test/functional/es_archives/reporting/bwc/6_2/data.json.gz differ diff --git a/x-pack/test/functional/es_archives/reporting/bwc/6_3/data.json.gz b/x-pack/test/functional/es_archives/reporting/bwc/6_3/data.json.gz index ccf2bcc6edc87..fd5e9a18216c6 100644 Binary files a/x-pack/test/functional/es_archives/reporting/bwc/6_3/data.json.gz and b/x-pack/test/functional/es_archives/reporting/bwc/6_3/data.json.gz differ diff --git a/x-pack/test_serverless/api_integration/test_suites/common/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/index.ts index de30854beccfc..3ca6b715102d9 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/index.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/index.ts @@ -13,5 +13,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./spaces')); loadTestFile(require.resolve('./security_response_headers')); loadTestFile(require.resolve('./rollups')); + loadTestFile(require.resolve('./index_management')); }); } diff --git a/x-pack/test_serverless/api_integration/test_suites/common/index_management/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/index_management/index.ts new file mode 100644 index 0000000000000..dd7d8bc20e624 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/index_management/index.ts @@ -0,0 +1,14 @@ +/* + * 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 { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('Index Management APIs', function () { + loadTestFile(require.resolve('./index_templates')); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/index_management/index_templates.ts b/x-pack/test_serverless/api_integration/test_suites/common/index_management/index_templates.ts new file mode 100644 index 0000000000000..a4e082387ab4a --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/index_management/index_templates.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from 'expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +const API_BASE_PATH = '/api/index_management'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('es'); + const log = getService('log'); + + describe('Index templates', function () { + const templateName = `template-${Math.random()}`; + const indexTemplate = { + name: templateName, + body: { + index_patterns: ['test*'], + }, + }; + + before(async () => { + // Create a new index template to test against + try { + await es.indices.putIndexTemplate(indexTemplate); + } catch (err) { + log.debug('[Setup error] Error creating index template'); + throw err; + } + }); + + after(async () => { + // Cleanup template created for testing purposes + try { + await es.indices.deleteIndexTemplate({ + name: templateName, + }); + } catch (err) { + log.debug('[Cleanup error] Error deleting index template'); + throw err; + } + }); + + describe('get all', () => { + it('should list all the index templates with the expected parameters', async () => { + const { body: allTemplates } = await supertest + .get(`${API_BASE_PATH}/index_templates`) + .set('kbn-xsrf', 'xxx') + .set('x-elastic-internal-origin', 'xxx') + .expect(200); + + // Legacy templates are not applicable on serverless + expect(allTemplates.legacyTemplates.length).toEqual(0); + + const indexTemplateFound = allTemplates.templates.find( + (template: { name: string }) => template.name === indexTemplate.name + ); + + expect(indexTemplateFound).toBeTruthy(); + + const expectedKeys = [ + 'name', + 'indexPatterns', + 'hasSettings', + 'hasAliases', + 'hasMappings', + '_kbnMeta', + ].sort(); + + expect(Object.keys(indexTemplateFound).sort()).toEqual(expectedKeys); + }); + }); + + describe('get one', () => { + it('should return an index template with the expected parameters', async () => { + const { body } = await supertest + .get(`${API_BASE_PATH}/index_templates/${templateName}`) + .set('kbn-xsrf', 'xxx') + .set('x-elastic-internal-origin', 'xxx') + .expect(200); + + const expectedKeys = ['name', 'indexPatterns', 'template', '_kbnMeta'].sort(); + + expect(body.name).toEqual(templateName); + expect(Object.keys(body).sort()).toEqual(expectedKeys); + }); + }); + }); +} diff --git a/x-pack/test_serverless/functional/config.base.ts b/x-pack/test_serverless/functional/config.base.ts index 23739a9615e69..640ae2402b544 100644 --- a/x-pack/test_serverless/functional/config.base.ts +++ b/x-pack/test_serverless/functional/config.base.ts @@ -55,6 +55,9 @@ export function createTestConfig(options: CreateTestConfigOptions) { management: { pathname: '/app/management', }, + indexManagement: { + pathname: '/app/management/data/index_management', + }, }, // choose where screenshots should be saved screenshots: { diff --git a/x-pack/test_serverless/functional/test_suites/common/index.ts b/x-pack/test_serverless/functional/test_suites/common/index.ts index 31497afb8c7d8..7150589527b04 100644 --- a/x-pack/test_serverless/functional/test_suites/common/index.ts +++ b/x-pack/test_serverless/functional/test_suites/common/index.ts @@ -14,5 +14,8 @@ export default function ({ loadTestFile }: FtrProviderContext) { // platform security loadTestFile(require.resolve('./security/navigation/avatar_menu')); + + // Management + loadTestFile(require.resolve('./index_management')); }); } diff --git a/x-pack/plugins/spaces/public/advanced_settings/components/index.ts b/x-pack/test_serverless/functional/test_suites/common/index_management/index.ts similarity index 51% rename from x-pack/plugins/spaces/public/advanced_settings/components/index.ts rename to x-pack/test_serverless/functional/test_suites/common/index_management/index.ts index d261a136525e2..52472972a1faa 100644 --- a/x-pack/plugins/spaces/public/advanced_settings/components/index.ts +++ b/x-pack/test_serverless/functional/test_suites/common/index_management/index.ts @@ -5,5 +5,10 @@ * 2.0. */ -export { AdvancedSettingsSubtitle } from './advanced_settings_subtitle'; -export { AdvancedSettingsTitle } from './advanced_settings_title'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default ({ loadTestFile }: FtrProviderContext) => { + describe('Index Management', function () { + loadTestFile(require.resolve('./index_templates')); + }); +}; diff --git a/x-pack/test_serverless/functional/test_suites/common/index_management/index_templates.ts b/x-pack/test_serverless/functional/test_suites/common/index_management/index_templates.ts new file mode 100644 index 0000000000000..26feb519a39a8 --- /dev/null +++ b/x-pack/test_serverless/functional/test_suites/common/index_management/index_templates.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const testSubjects = getService('testSubjects'); + const pageObjects = getPageObjects(['common', 'indexManagement', 'header']); + const browser = getService('browser'); + const security = getService('security'); + const retry = getService('retry'); + + describe('Index Templates', function () { + before(async () => { + await security.testUser.setRoles(['index_management_user']); + await pageObjects.common.navigateToApp('indexManagement'); + // Navigate to the index templates tab + await pageObjects.indexManagement.changeTabs('templatesTab'); + }); + + it('renders the index templates tab', async () => { + await retry.waitFor('index templates list to be visible', async () => { + return await testSubjects.exists('templateList'); + }); + + const url = await browser.getCurrentUrl(); + expect(url).to.contain(`/templates`); + }); + }); +}; diff --git a/yarn.lock b/yarn.lock index 5a72d2a9a2534..8fa1a826bb7e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4784,6 +4784,10 @@ version "0.0.0" uid "" +"@kbn/management-settings-section-registry@link:packages/kbn-management/settings/section_registry": + version "0.0.0" + uid "" + "@kbn/management-storybook-config@link:packages/kbn-management/storybook/config": version "0.0.0" uid ""