diff --git a/.buildkite/scripts/steps/storybooks/build_and_upload.js b/.buildkite/scripts/steps/storybooks/build_and_upload.js index 0fdf24d87ffad..89958fe08d6cc 100644 --- a/.buildkite/scripts/steps/storybooks/build_and_upload.js +++ b/.buildkite/scripts/steps/storybooks/build_and_upload.js @@ -8,6 +8,7 @@ const STORYBOOKS = [ 'canvas', 'codeeditor', 'ci_composite', + 'custom_integrations', 'url_template_editor', 'dashboard', 'dashboard_enhanced', diff --git a/.ci/.storybook/main.js b/.ci/.storybook/main.js index e399ec087e168..37f3391337308 100644 --- a/.ci/.storybook/main.js +++ b/.ci/.storybook/main.js @@ -11,6 +11,12 @@ const aliases = require('../../src/dev/storybook/aliases.ts').storybookAliases; config.refs = {}; +// Required due to https://github.com/storybookjs/storybook/issues/13834 +config.babel = async (options) => ({ + ...options, + plugins: ['@babel/plugin-transform-typescript', ...options.plugins], +}); + for (const alias of Object.keys(aliases).filter((a) => a !== 'ci_composite')) { // snake_case -> Title Case const title = alias diff --git a/.i18nrc.json b/.i18nrc.json index 45016edc38dcd..46d2f8c6a23bf 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -5,6 +5,7 @@ "kbnConfig": "packages/kbn-config/src", "console": "src/plugins/console", "core": "src/core", + "customIntegrations": "src/plugins/custom_integrations", "discover": "src/plugins/discover", "bfetch": "src/plugins/bfetch", "dashboard": "src/plugins/dashboard", @@ -18,6 +19,7 @@ "home": "src/plugins/home", "flot": "packages/kbn-ui-shared-deps-src/src/flot_charts", "charts": "src/plugins/charts", + "customIntegrations": "src/plugins/custom_integrations", "esUi": "src/plugins/es_ui_shared", "devTools": "src/plugins/dev_tools", "expressions": "src/plugins/expressions", diff --git a/dev_docs/key_concepts/kibana_platform_plugin_intro.mdx b/dev_docs/key_concepts/kibana_platform_plugin_intro.mdx index 617944118cdbd..133b96f44da88 100644 --- a/dev_docs/key_concepts/kibana_platform_plugin_intro.mdx +++ b/dev_docs/key_concepts/kibana_platform_plugin_intro.mdx @@ -17,13 +17,13 @@ already existing applications. Did you know that almost everything you see in th Kibana UI is built inside a plugin? If you removed all plugins from Kibana, you'd be left with an empty navigation menu, and a set of developer tools. The Kibana platform is a blank canvas, just waiting for a developer to come along and create something! -![Kibana personas](assets/kibana_platform_plugin_end_user.png) +![Kibana personas](../assets/kibana_platform_plugin_end_user.png) ## 1,000 foot view At a super high-level, Kibana is composed of **plugins**, **core**, and **Kibana packages**. -![Kibana 1000 ft arch](assets/1000_ft_arch.png) +![Kibana 1000 ft arch](../assets/1000_ft_arch.png) **Plugins** provide the majority of all functionality in Kibana. All applications and UIs are defined here. @@ -51,7 +51,7 @@ We try to put only the most stable and fundamental code into `Core`, while optio Today it looks something like this. -![Core vs platform plugins vs plugins](assets/platform_plugins_core.png) +![Core vs platform plugins vs plugins](../assets/platform_plugins_core.png) "Platform plugins" provide core-like functionality, just outside of core, and their public APIs tend to be more volatile. Other plugins may still expose shared services, but they are intended only for usage by a small subset of specific plugins, and may not be generic or "platform-like". @@ -91,7 +91,7 @@ A plugin may register many applications, or none. Applications are top level pages in the Kibana UI. Dashboard, Canvas, Maps, App Search, etc, are all examples of applications: -![applications in kibana](./assets/applications.png) +![applications in kibana](../assets/applications.png) A plugin can register an application by adding it to core's application . diff --git a/docs/dev-tools/grokdebugger/index.asciidoc b/docs/dev-tools/grokdebugger/index.asciidoc index 934452c54ccca..6a809c13fcb93 100644 --- a/docs/dev-tools/grokdebugger/index.asciidoc +++ b/docs/dev-tools/grokdebugger/index.asciidoc @@ -9,21 +9,22 @@ structure it. Grok is good for parsing syslog, apache, and other webserver logs, mysql logs, and in general, any log format that is written for human consumption. -Grok patterns are supported in the ingest node -{ref}/grok-processor.html[grok processor] and the Logstash -{logstash-ref}/plugins-filters-grok.html[grok filter]. See -{logstash-ref}/plugins-filters-grok.html#_grok_basics[grok basics] -for more information on the syntax for a grok pattern. - -The Elastic Stack ships -with more than 120 reusable grok patterns. See -https://github.com/elastic/elasticsearch/tree/master/libs/grok/src/main/resources/patterns[Ingest node grok patterns] and https://github.com/logstash-plugins/logstash-patterns-core/tree/master/patterns[Logstash grok patterns] -for the complete list of patterns. +Grok patterns are supported in {es} {ref}/runtime.html[runtime fields], the {es} +{ref}/grok-processor.html[grok ingest processor], and the {ls} +{logstash-ref}/plugins-filters-grok.html[grok filter]. For syntax, see +{ref}/grok.html[Grokking grok]. + +The {stack} ships with more than 120 reusable grok patterns. For a complete +list of patterns, see +https://github.com/elastic/elasticsearch/tree/master/libs/grok/src/main/resources/patterns[{es} +grok patterns] and +https://github.com/logstash-plugins/logstash-patterns-core/tree/master/patterns[{ls} +grok patterns]. Because -ingest node and Logstash share the same grok implementation and pattern +{es} and {ls} share the same grok implementation and pattern libraries, any grok pattern that you create in the *Grok Debugger* will work -in ingest node and Logstash. +in both {es} and {ls}. [float] [[grokdebugger-getting-started]] diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 7f7041f7815cd..cbf46801fa86f 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -458,7 +458,7 @@ the infrastructure monitoring use-case within Kibana. |{kib-repo}blob/{branch}/x-pack/plugins/ingest_pipelines/README.md[ingestPipelines] -|The ingest_pipelines plugin provides Kibana support for Elasticsearch's ingest nodes. Please refer to the Elasticsearch documentation for more details. +|The ingest_pipelines plugin provides Kibana support for Elasticsearch's ingest pipelines. |{kib-repo}blob/{branch}/x-pack/plugins/lens/readme.md[lens] diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc index 4c0c335b3c33e..60a65580501a6 100644 --- a/docs/migration/migrate_8_0.asciidoc +++ b/docs/migration/migrate_8_0.asciidoc @@ -364,4 +364,34 @@ Configuration management tools and automation will need to be updated to use the === `server.xsrf.token` is no longer valid *Details:* The deprecated `server.xsrf.token` setting in the `kibana.yml` file has been removed. +[float] +=== `newsfeed.defaultLanguage` is no longer valid +*Details:* Specifying a default language to retrieve newsfeed items is no longer supported. + +*Impact:* Newsfeed items will be retrieved based on the browser locale and fallback to 'en' if an item does not have a translation for the locale. Configure {kibana-ref}/i18n-settings-kb.html#general-i18n-settings-kb[`i18n.locale`] to override the default behavior. + +[float] +=== `xpack.banners.placement` has changed value +*Details:* `xpack.banners.placement: 'header'` setting in `kibana.yml` has changed value. + +*Impact:* Use {kibana-ref}/banners-settings-kb.html#banners-settings-kb[`xpack.banners.placement: 'top'`] instead. + +[float] +=== `cpu.cgroup.path.override` is no longer valid +*Details:* The deprecated `cpu.cgroup.path.override` setting is no longer supported. + +*Impact:* Configure {kibana-ref}/settings.html#ops-cGroupOverrides-cpuPath[`ops.cGroupOverrides.cpuPath`] instead. + +[float] +=== `cpuacct.cgroup.path.override` is no longer valid +*Details:* The deprecated `cpuacct.cgroup.path.override` setting is no longer supported. + +*Impact:* Configure {kibana-ref}/settings.html#ops-cGroupOverrides-cpuAcctPath[`ops.cGroupOverrides.cpuAcctPath`] instead. + +[float] +=== `server.xsrf.whitelist` is no longer valid +*Details:* The deprecated `server.xsrf.whitelist` setting is no longer supported. + +*Impact:* Use {kibana-ref}/settings.html#settings-xsrf-allowlist[`server.xsrf.allowlist`] instead. + // end::notable-breaking-changes[] diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 97a87506f2337..d5bc2ccd8ef7d 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -293,7 +293,7 @@ This content has moved. Refer to <>. This content has moved. Refer to <>. [role="exclude",id="ingest-node-pipelines"] -== Ingest Node Pipelines +== Ingest Pipelines This content has moved. Refer to {ref}/ingest.html[Ingest pipelines]. diff --git a/docs/settings/alert-action-settings.asciidoc b/docs/settings/alert-action-settings.asciidoc index 92357a8800d67..3a94e652d2ea0 100644 --- a/docs/settings/alert-action-settings.asciidoc +++ b/docs/settings/alert-action-settings.asciidoc @@ -31,11 +31,6 @@ Be sure to back up the encryption key value somewhere safe, as your alerting rul [[action-settings]] ==== Action settings -`xpack.actions.enabled`:: -deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] -Feature toggle that enables Actions in {kib}. -If `false`, all features dependent on Actions are disabled, including the *Observability* and *Security* apps. Default: `true`. - `xpack.actions.allowedHosts` {ess-icon}:: A list of hostnames that {kib} is allowed to connect to when built-in actions are triggered. It defaults to `[*]`, allowing any host, but keep in mind the potential for SSRF attacks when hosts are not explicitly added to the allowed hosts. An empty list `[]` can be used to block built-in actions from making any external connections. + @@ -179,3 +174,10 @@ For example, `20m`, `24h`, `7d`, `1w`. Default: `60s`. `xpack.alerting.maxEphemeralActionsPerAlert`:: Sets the number of actions that will be executed ephemerally. To use this, enable ephemeral tasks in task manager first with <> + +`xpack.alerting.defaultRuleTaskTimeout`:: +Specifies the default timeout for the all rule types tasks. The time is formatted as: ++ +`[ms,s,m,h,d,w,M,Y]` ++ +For example, `20m`, `24h`, `7d`, `1w`. Default: `60s`. diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index 694f8c53f6745..560f2d850c6d5 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -11,7 +11,6 @@ You can configure `xpack.reporting` settings in your `kibana.yml` to: * <> * <> -* <> * <> * <> * <> @@ -47,33 +46,6 @@ The static encryption key for reporting. Use an alphanumeric text string that is xpack.reporting.encryptionKey: "something_secret" -------------------------------------------------------------------------------- -[float] -[[report-indices]] -==== Reporting index setting - - - -`xpack.reporting.index`:: -deprecated:[7.11.0,This setting will be removed in 8.0.0.] Multitenancy by changing `kibana.index` is unsupported starting in 8.0.0. For more details, refer to https://ela.st/kbn-remove-legacy-multitenancy[8.0 Breaking Changes]. When you divide workspaces in an Elastic cluster using multiple {kib} instances with a different `kibana.index` setting per instance, you must set a unique `xpack.reporting.index` setting per `kibana.index`. Otherwise, report generation periodically fails if a report is queued through an instance with one `kibana.index` setting, and an instance with a different `kibana.index` attempts to claim the job. Reporting uses a weekly index in {es} to store the reporting job and the report content. The index is automatically created if it does not already exist. Configure a unique value for `xpack.reporting.index`, beginning with `.reporting-`, for every {kib} instance that has a unique <> setting. Defaults to `.reporting`. - -{kib} instance A: -[source,yaml] --------------------------------------------------------------------------------- -kibana.index: ".kibana-a" -xpack.reporting.index: ".reporting-a" -xpack.reporting.encryptionKey: "something_secret" --------------------------------------------------------------------------------- - -{kib} instance B: -[source,yaml] --------------------------------------------------------------------------------- -kibana.index: ".kibana-b" -xpack.reporting.index: ".reporting-b" -xpack.reporting.encryptionKey: "something_secret" --------------------------------------------------------------------------------- - -NOTE: If security is enabled, the `xpack.reporting.index` setting should begin with `.reporting-` for the `kibana_system` role to have the necessary privileges over the index. - [float] [[reporting-kibana-server-settings]] ==== {kib} server settings diff --git a/docs/setup/configuring-reporting.asciidoc b/docs/setup/configuring-reporting.asciidoc index 6d209092d3338..38bf2955fb56e 100644 --- a/docs/setup/configuring-reporting.asciidoc +++ b/docs/setup/configuring-reporting.asciidoc @@ -148,56 +148,6 @@ reporting_user: - "cn=Bill Murray,dc=example,dc=com" -------------------------------------------------------------------------------- -[float] -==== Grant access with a custom index - -If you are using a custom index, the `xpack.reporting.index` setting must begin with `.reporting-*`. The default {kib} system user has `all` privileges against the `.reporting-*` pattern of indices. - -If you use a different pattern for the `xpack.reporting.index` setting, you must create a custom `kibana_system` user with appropriate access to the index. - -NOTE: In the next major version of Kibana, granting access with a custom index is unsupported. - -. Create the reporting role. - -.. Open the main menu, then click *Stack Management*. - -.. Click *Roles > Create role*. - -. Specify the role settings. - -.. Enter the *Role name*. For example, `custom-reporting-user`. - -.. From the *Indices* dropdown, select the custom index. - -.. From the *Privileges* dropdown, select *all*. - -.. Click *Add Kibana privilege*. - -.. Select one or more *Spaces* that you want to grant reporting privileges to. - -.. Click *Customize*, then click *Analytics*. - -.. Next to each application you want to grant reporting privileges to, click *All*. - -.. Click *Add {kib} privilege*, then click *Create role*. - -. Assign the reporting role to a user. - -.. Open the main menu, then click *Stack Management*. - -.. Click *Users*, then click the user you want to assign the reporting role to. - -.. From the *Roles* dropdown, select *kibana_system* and *custom-reporting-user*. - -.. Click *Update user*. - -. Configure {kib} to use the new account. -+ -[source,js] --------------------------------------------------------------------------------- -elasticsearch.username: 'custom_kibana_system' --------------------------------------------------------------------------------- - [float] [[securing-reporting]] === Secure the reporting endpoints diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index c098fb697de04..7a85411065db6 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -26,16 +26,6 @@ Toggling this causes the server to regenerate assets on the next startup, which may cause a delay before pages start being served. Set to `false` to disable Console. *Default: `true`* -| `cpu.cgroup.path.override:` - | deprecated:[7.10.0,"In 8.0 and later, this setting will no longer be supported."] - This setting has been renamed to - <>. - -| `cpuacct.cgroup.path.override:` - | deprecated:[7.10.0,"In 8.0 and later, this setting will no longer be supported."] - This setting has been renamed to - <>. - | `csp.rules:` | deprecated:[7.14.0,"In 8.0 and later, this setting will no longer be supported."] A https://w3c.github.io/webappsec-csp/[Content Security Policy] template diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index 4e5f70db9aef6..1f38d50e2d0bd 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -17,7 +17,7 @@ Consult your administrator if you do not have the appropriate access. [cols="50, 50"] |=== -| {ref}/ingest.html[Ingest Node Pipelines] +| {ref}/ingest.html[Ingest Pipelines] | Create and manage ingest pipelines that let you perform common transformations and enrichments on your data. diff --git a/docs/user/monitoring/monitoring-metricbeat.asciidoc b/docs/user/monitoring/monitoring-metricbeat.asciidoc index 5ef3b8177a9c5..101377e047588 100644 --- a/docs/user/monitoring/monitoring-metricbeat.asciidoc +++ b/docs/user/monitoring/monitoring-metricbeat.asciidoc @@ -189,8 +189,9 @@ If you configured the monitoring cluster to use encrypted communications, you must access it via HTTPS. For example, use a `hosts` setting like `https://es-mon-1:9200`. -IMPORTANT: The {es} {monitor-features} use ingest pipelines, therefore the -cluster that stores the monitoring data must have at least one ingest node. +IMPORTANT: The {es} {monitor-features} use ingest pipelines. The +cluster that stores the monitoring data must have at least one node with the +`ingest` role. If the {es} {security-features} are enabled on the monitoring cluster, you must provide a valid user ID and password so that {metricbeat} can send metrics diff --git a/package.json b/package.json index 37a5239cf068a..6539189ca994d 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,9 @@ "yarn": "^1.21.1" }, "dependencies": { + "@dnd-kit/core": "^3.1.1", + "@dnd-kit/sortable": "^4.0.0", + "@dnd-kit/utilities": "^2.0.0", "@babel/runtime": "^7.15.4", "@elastic/apm-rum": "^5.9.1", "@elastic/apm-rum-react": "^1.3.1", diff --git a/packages/kbn-rule-data-utils/src/technical_field_names.ts b/packages/kbn-rule-data-utils/src/technical_field_names.ts index 86a036bbb9fe2..6ac897bbafb08 100644 --- a/packages/kbn-rule-data-utils/src/technical_field_names.ts +++ b/packages/kbn-rule-data-utils/src/technical_field_names.ts @@ -52,6 +52,7 @@ const ALERT_RULE_LICENSE = `${ALERT_RULE_NAMESPACE}.license` as const; const ALERT_RULE_CATEGORY = `${ALERT_RULE_NAMESPACE}.category` as const; const ALERT_RULE_NAME = `${ALERT_RULE_NAMESPACE}.name` as const; const ALERT_RULE_NOTE = `${ALERT_RULE_NAMESPACE}.note` as const; +const ALERT_RULE_PARAMS = `${ALERT_RULE_NAMESPACE}.params` as const; const ALERT_RULE_REFERENCES = `${ALERT_RULE_NAMESPACE}.references` as const; const ALERT_RULE_RISK_SCORE = `${ALERT_RULE_NAMESPACE}.risk_score` as const; const ALERT_RULE_RISK_SCORE_MAPPING = `${ALERT_RULE_NAMESPACE}.risk_score_mapping` as const; @@ -109,6 +110,7 @@ const fields = { ALERT_RULE_LICENSE, ALERT_RULE_NAME, ALERT_RULE_NOTE, + ALERT_RULE_PARAMS, ALERT_RULE_REFERENCES, ALERT_RULE_RISK_SCORE, ALERT_RULE_RISK_SCORE_MAPPING, @@ -164,6 +166,7 @@ export { ALERT_RULE_LICENSE, ALERT_RULE_NAME, ALERT_RULE_NOTE, + ALERT_RULE_PARAMS, ALERT_RULE_REFERENCES, ALERT_RULE_RISK_SCORE, ALERT_RULE_RISK_SCORE_MAPPING, diff --git a/packages/kbn-utils/src/path/index.ts b/packages/kbn-utils/src/path/index.ts index 9ee699c22c30c..15d6a3eddf01e 100644 --- a/packages/kbn-utils/src/path/index.ts +++ b/packages/kbn-utils/src/path/index.ts @@ -15,14 +15,12 @@ const isString = (v: any): v is string => typeof v === 'string'; const CONFIG_PATHS = [ process.env.KBN_PATH_CONF && join(process.env.KBN_PATH_CONF, 'kibana.yml'), - process.env.KIBANA_PATH_CONF && join(process.env.KIBANA_PATH_CONF, 'kibana.yml'), // deprecated join(REPO_ROOT, 'config/kibana.yml'), '/etc/kibana/kibana.yml', ].filter(isString); const CONFIG_DIRECTORIES = [ process.env.KBN_PATH_CONF, - process.env.KIBANA_PATH_CONF, // deprecated join(REPO_ROOT, 'config'), '/etc/kibana', ].filter(isString); diff --git a/src/core/server/config/deprecation/core_deprecations.test.ts b/src/core/server/config/deprecation/core_deprecations.test.ts index 4e99f46ea05ff..95e23561a9378 100644 --- a/src/core/server/config/deprecation/core_deprecations.test.ts +++ b/src/core/server/config/deprecation/core_deprecations.test.ts @@ -19,38 +19,6 @@ describe('core deprecations', () => { process.env = { ...initialEnv }; }); - describe('kibanaPathConf', () => { - it('logs a warning if KIBANA_PATH_CONF environ variable is set', () => { - process.env.KIBANA_PATH_CONF = 'somepath'; - const { messages } = applyCoreDeprecations(); - expect(messages).toMatchInlineSnapshot(` - Array [ - "Environment variable \\"KIBANA_PATH_CONF\\" is deprecated. It has been replaced with \\"KBN_PATH_CONF\\" pointing to a config folder", - ] - `); - }); - - it('does not log a warning if KIBANA_PATH_CONF environ variable is unset', () => { - delete process.env.KIBANA_PATH_CONF; - const { messages } = applyCoreDeprecations(); - expect(messages).toHaveLength(0); - }); - }); - - describe('xsrfDeprecation', () => { - it('logs a warning if server.xsrf.whitelist is set', () => { - const { migrated, messages } = applyCoreDeprecations({ - server: { xsrf: { whitelist: ['/path'] } }, - }); - expect(migrated.server.xsrf.allowlist).toEqual(['/path']); - expect(messages).toMatchInlineSnapshot(` - Array [ - "Setting \\"server.xsrf.whitelist\\" has been replaced by \\"server.xsrf.allowlist\\"", - ] - `); - }); - }); - describe('server.cors', () => { it('renames server.cors to server.cors.enabled', () => { const { migrated } = applyCoreDeprecations({ @@ -58,8 +26,9 @@ describe('core deprecations', () => { }); expect(migrated.server.cors).toEqual({ enabled: true }); }); + it('logs a warning message about server.cors renaming', () => { - const { messages } = applyCoreDeprecations({ + const { messages, levels } = applyCoreDeprecations({ server: { cors: true }, }); expect(messages).toMatchInlineSnapshot(` @@ -67,7 +36,13 @@ describe('core deprecations', () => { "\\"server.cors\\" is deprecated and has been replaced by \\"server.cors.enabled\\"", ] `); + expect(levels).toMatchInlineSnapshot(` + Array [ + "warning", + ] + `); }); + it('does not log deprecation message when server.cors.enabled set', () => { const { migrated, messages } = applyCoreDeprecations({ server: { cors: { enabled: true } }, diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index 674812bd0957b..4e5f711fe9f3a 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -8,19 +8,6 @@ import { ConfigDeprecationProvider, ConfigDeprecation } from '@kbn/config'; -const kibanaPathConf: ConfigDeprecation = (settings, fromPath, addDeprecation) => { - if (process.env?.KIBANA_PATH_CONF) { - addDeprecation({ - message: `Environment variable "KIBANA_PATH_CONF" is deprecated. It has been replaced with "KBN_PATH_CONF" pointing to a config folder`, - correctiveActions: { - manualSteps: [ - 'Use "KBN_PATH_CONF" instead of "KIBANA_PATH_CONF" to point to a config folder.', - ], - }, - }); - } -}; - const rewriteBasePathDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (settings.server?.basePath && !settings.server?.rewriteBasePath) { addDeprecation({ @@ -44,6 +31,7 @@ const rewriteCorsSettings: ConfigDeprecation = (settings, fromPath, addDeprecati if (typeof corsSettings === 'boolean') { addDeprecation({ message: '"server.cors" is deprecated and has been replaced by "server.cors.enabled"', + level: 'warning', correctiveActions: { manualSteps: [ `Replace "server.cors: ${corsSettings}" with "server.cors.enabled: ${corsSettings}"`, @@ -114,11 +102,7 @@ const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecati }; export const coreDeprecationProvider: ConfigDeprecationProvider = ({ rename, unusedFromRoot }) => [ - rename('cpu.cgroup.path.override', 'ops.cGroupOverrides.cpuPath'), - rename('cpuacct.cgroup.path.override', 'ops.cGroupOverrides.cpuAcctPath'), - rename('server.xsrf.whitelist', 'server.xsrf.allowlist'), rewriteCorsSettings, - kibanaPathConf, rewriteBasePathDeprecation, cspRulesDeprecation, ]; diff --git a/src/core/server/config/test_utils.ts b/src/core/server/config/test_utils.ts index e3f9ca7eb29f2..592ea0981682f 100644 --- a/src/core/server/config/test_utils.ts +++ b/src/core/server/config/test_utils.ts @@ -16,6 +16,7 @@ function collectDeprecations( ) { const deprecations = provider(configDeprecationFactory); const deprecationMessages: string[] = []; + const deprecationLevels: string[] = []; const { config: migrated } = applyDeprecations( settings, deprecations.map((deprecation) => ({ @@ -23,11 +24,14 @@ function collectDeprecations( path, })), () => - ({ message }) => - deprecationMessages.push(message) + ({ message, level }) => { + deprecationMessages.push(message); + deprecationLevels.push(level ?? ''); + } ); return { messages: deprecationMessages, + levels: deprecationLevels, migrated, }; } diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index f671fa963079f..dd5b66af9ef21 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -26,8 +26,6 @@ kibana_vars=( console.enabled console.proxyConfig console.proxyFilter - cpu.cgroup.path.override - cpuacct.cgroup.path.override csp.rules csp.strict csp.warnLegacyBrowsers @@ -175,7 +173,6 @@ kibana_vars=( server.uuid server.xsrf.allowlist server.xsrf.disableProtection - server.xsrf.whitelist status.allowAnonymous status.v6ApiFormat telemetry.allowChangingOptInStatus @@ -194,7 +191,6 @@ kibana_vars=( vis_type_vega.enableExternalUrls xpack.actions.allowedHosts xpack.actions.customHostSettings - xpack.actions.enabled xpack.actions.enabledActionTypes xpack.actions.maxResponseContentLength xpack.actions.preconfigured @@ -211,6 +207,7 @@ kibana_vars=( xpack.alerting.healthCheck.interval xpack.alerting.invalidateApiKeysTask.interval xpack.alerting.invalidateApiKeysTask.removalDelay + xpack.alerting.defaultRuleTaskTimeout xpack.alerts.healthCheck.interval xpack.alerts.invalidateApiKeysTask.interval xpack.alerts.invalidateApiKeysTask.removalDelay @@ -251,7 +248,6 @@ kibana_vars=( xpack.discoverEnhanced.actions.exploreDataInContextMenu.enabled xpack.encryptedSavedObjects.encryptionKey xpack.encryptedSavedObjects.keyRotation.decryptionOnlyKeys - xpack.event_log.enabled xpack.event_log.indexEntries xpack.event_log.logEntries xpack.fleet.agentPolicies @@ -318,7 +314,6 @@ kibana_vars=( xpack.reporting.csv.useByteOrderMarkEncoding xpack.reporting.enabled xpack.reporting.encryptionKey - xpack.reporting.index xpack.reporting.kibanaApp xpack.reporting.kibanaServer.hostname xpack.reporting.kibanaServer.port @@ -384,7 +379,6 @@ kibana_vars=( xpack.securitySolution.prebuiltRulesFromSavedObjects xpack.spaces.enabled xpack.spaces.maxSpaces - xpack.task_manager.enabled xpack.task_manager.index xpack.task_manager.max_attempts xpack.task_manager.max_poll_inactivity_cycles @@ -426,7 +420,7 @@ umask 0002 # paths. Therefore, Kibana provides a mechanism to override # reading the cgroup path from /proc/self/cgroup and instead uses the # cgroup path defined the configuration properties -# cpu.cgroup.path.override and cpuacct.cgroup.path.override. +# ops.cGroupOverrides.cpuPath and ops.cGroupOverrides.cpuAcctPath. # Therefore, we set this value here so that cgroup statistics are # available for the container this process will run in. diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts index a61a2618d6428..c04f0d4f9320f 100644 --- a/src/dev/storybook/aliases.ts +++ b/src/dev/storybook/aliases.ts @@ -12,6 +12,7 @@ export const storybookAliases = { canvas: 'x-pack/plugins/canvas/storybook', codeeditor: 'src/plugins/kibana_react/public/code_editor/.storybook', ci_composite: '.ci/.storybook', + custom_integrations: 'src/plugins/custom_integrations/storybook', url_template_editor: 'src/plugins/kibana_react/public/url_template_editor/.storybook', dashboard: 'src/plugins/dashboard/.storybook', dashboard_enhanced: 'x-pack/plugins/dashboard_enhanced/.storybook', diff --git a/src/plugins/custom_integrations/common/index.ts b/src/plugins/custom_integrations/common/index.ts index 73e15c91ce4bf..e2408d3124604 100755 --- a/src/plugins/custom_integrations/common/index.ts +++ b/src/plugins/custom_integrations/common/index.ts @@ -15,6 +15,7 @@ export interface IntegrationCategoryCount { } export const INTEGRATION_CATEGORY_DISPLAY = { + // Known EPR aws: 'AWS', azure: 'Azure', cloud: 'Cloud', @@ -39,8 +40,12 @@ export const INTEGRATION_CATEGORY_DISPLAY = { ticketing: 'Ticketing', version_control: 'Version control', web: 'Web', + + // Kibana added upload_file: 'Upload a file', + language_client: 'Language client', + // Internal updates_available: 'Updates available', }; diff --git a/src/plugins/custom_integrations/kibana.json b/src/plugins/custom_integrations/kibana.json index 3a78270d9ef09..cd58c1aec1ecb 100755 --- a/src/plugins/custom_integrations/kibana.json +++ b/src/plugins/custom_integrations/kibana.json @@ -12,5 +12,8 @@ "extraPublicDirs": [ "common" ], + "requiredPlugins": [ + "presentationUtil" + ], "optionalPlugins": [] } diff --git a/src/plugins/custom_integrations/public/assets/language_clients/dotnet.svg b/src/plugins/custom_integrations/public/assets/language_clients/dotnet.svg new file mode 100755 index 0000000000000..92a7ad45d9f9c --- /dev/null +++ b/src/plugins/custom_integrations/public/assets/language_clients/dotnet.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/plugins/custom_integrations/public/assets/language_clients/es.svg b/src/plugins/custom_integrations/public/assets/language_clients/es.svg new file mode 100755 index 0000000000000..b1224e212e098 --- /dev/null +++ b/src/plugins/custom_integrations/public/assets/language_clients/es.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/plugins/custom_integrations/public/assets/language_clients/go.svg b/src/plugins/custom_integrations/public/assets/language_clients/go.svg new file mode 100755 index 0000000000000..223a57194fd7c --- /dev/null +++ b/src/plugins/custom_integrations/public/assets/language_clients/go.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/plugins/custom_integrations/public/assets/language_clients/java.svg b/src/plugins/custom_integrations/public/assets/language_clients/java.svg new file mode 100644 index 0000000000000..d24d844695762 --- /dev/null +++ b/src/plugins/custom_integrations/public/assets/language_clients/java.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/plugins/custom_integrations/public/assets/language_clients/nodejs.svg b/src/plugins/custom_integrations/public/assets/language_clients/nodejs.svg new file mode 100755 index 0000000000000..4dd358743bbff --- /dev/null +++ b/src/plugins/custom_integrations/public/assets/language_clients/nodejs.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/custom_integrations/public/assets/language_clients/perl.svg b/src/plugins/custom_integrations/public/assets/language_clients/perl.svg new file mode 100755 index 0000000000000..6ef322a3f58ae --- /dev/null +++ b/src/plugins/custom_integrations/public/assets/language_clients/perl.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/plugins/custom_integrations/public/assets/language_clients/php.svg b/src/plugins/custom_integrations/public/assets/language_clients/php.svg new file mode 100755 index 0000000000000..7a1c20116f466 --- /dev/null +++ b/src/plugins/custom_integrations/public/assets/language_clients/php.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/custom_integrations/public/assets/language_clients/python.svg b/src/plugins/custom_integrations/public/assets/language_clients/python.svg new file mode 100755 index 0000000000000..b7234c439ced5 --- /dev/null +++ b/src/plugins/custom_integrations/public/assets/language_clients/python.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/plugins/custom_integrations/public/assets/language_clients/ruby.svg b/src/plugins/custom_integrations/public/assets/language_clients/ruby.svg new file mode 100755 index 0000000000000..5e515bc0dd98e --- /dev/null +++ b/src/plugins/custom_integrations/public/assets/language_clients/ruby.svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/custom_integrations/public/assets/language_clients/rust.svg b/src/plugins/custom_integrations/public/assets/language_clients/rust.svg new file mode 100755 index 0000000000000..82dcaf2ade93e --- /dev/null +++ b/src/plugins/custom_integrations/public/assets/language_clients/rust.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/plugins/custom_integrations/public/components/index.tsx b/src/plugins/custom_integrations/public/components/index.tsx new file mode 100644 index 0000000000000..cfbec7d6d5ae5 --- /dev/null +++ b/src/plugins/custom_integrations/public/components/index.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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, { Suspense, ComponentType, ReactElement, Ref } from 'react'; +import { EuiLoadingSpinner, EuiErrorBoundary } from '@elastic/eui'; + +/** + * A HOC which supplies React.Suspense with a fallback component, and a `EuiErrorBoundary` to contain errors. + * @param Component A component deferred by `React.lazy` + * @param fallback A fallback component to render while things load; default is `EuiLoadingSpinner` + */ +export const withSuspense =

( + Component: ComponentType

, + fallback: ReactElement | null = +) => + React.forwardRef((props: P, ref: Ref) => { + return ( + + + + + + ); + }); + +export const LazyReplacementCard = React.lazy(() => import('./replacement_card')); diff --git a/src/plugins/custom_integrations/public/components/replacement_card/index.ts b/src/plugins/custom_integrations/public/components/replacement_card/index.ts new file mode 100644 index 0000000000000..631dc1fcb2ba2 --- /dev/null +++ b/src/plugins/custom_integrations/public/components/replacement_card/index.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 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 { ReplacementCard } from './replacement_card'; + +export { ReplacementCard, Props } from './replacement_card'; + +// required for dynamic import using React.lazy() +// eslint-disable-next-line import/no-default-export +export default ReplacementCard; diff --git a/src/plugins/custom_integrations/public/components/replacement_card/replacement_card.component.tsx b/src/plugins/custom_integrations/public/components/replacement_card/replacement_card.component.tsx new file mode 100644 index 0000000000000..f66d13fb911b5 --- /dev/null +++ b/src/plugins/custom_integrations/public/components/replacement_card/replacement_card.component.tsx @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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. + */ +/** @jsx jsx */ + +import { css, jsx } from '@emotion/react'; + +import { + htmlIdGenerator, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiText, + EuiAccordion, + EuiLink, + useEuiTheme, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { CustomIntegration } from '../../../common'; +import { usePlatformService } from '../../services'; + +export interface Props { + replacements: Array>; +} + +// TODO - clintandrewhall: should use doc-links service +const URL_COMPARISON = 'https://ela.st/beats-agent-comparison'; + +const idGenerator = htmlIdGenerator('replacementCard'); +const alsoAvailable = i18n.translate('customIntegrations.components.replacementAccordionLabel', { + defaultMessage: 'Also available in Beats', +}); + +const link = ( + + + +); + +/** + * A pure component, an accordion panel which can display information about replacements for a given EPR module. + */ +export const ReplacementCard = ({ replacements }: Props) => { + const { euiTheme } = useEuiTheme(); + const { getAbsolutePath } = usePlatformService(); + + if (replacements.length === 0) { + return null; + } + + const buttons = replacements.map((replacement) => ( + + + + {replacement.title} + + + + )); + + return ( +

+ + + + + + + + + + + {buttons} + + + + + +
+ ); +}; diff --git a/src/plugins/custom_integrations/public/components/replacement_card/replacement_card.stories.tsx b/src/plugins/custom_integrations/public/components/replacement_card/replacement_card.stories.tsx new file mode 100644 index 0000000000000..8fa0674c9b467 --- /dev/null +++ b/src/plugins/custom_integrations/public/components/replacement_card/replacement_card.stories.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { Meta } from '@storybook/react'; + +import { ReplacementCard as ConnectedComponent } from './replacement_card'; +import { ReplacementCard as PureComponent } from './replacement_card.component'; + +export default { + title: 'Replacement Card', + description: + 'An accordion panel which can display information about Beats alternatives to a given EPR module, (if available)', + decorators: [ + (storyFn, { globals }) => ( +
+ {storyFn()} +
+ ), + ], +} as Meta; + +interface Args { + eprPackageName: string; +} + +const args: Args = { + eprPackageName: 'nginx', +}; + +const argTypes = { + eprPackageName: { + control: { + type: 'radio', + options: ['nginx', 'okta', 'aws', 'apache'], + }, + }, +}; + +export function ReplacementCard({ eprPackageName }: Args) { + return ; +} + +ReplacementCard.args = args; +ReplacementCard.argTypes = argTypes; + +export function Component() { + return ( + + ); +} diff --git a/src/plugins/custom_integrations/public/components/replacement_card/replacement_card.tsx b/src/plugins/custom_integrations/public/components/replacement_card/replacement_card.tsx new file mode 100644 index 0000000000000..3e829270773a6 --- /dev/null +++ b/src/plugins/custom_integrations/public/components/replacement_card/replacement_card.tsx @@ -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 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 useAsync from 'react-use/lib/useAsync'; +import { useFindService } from '../../services'; + +import { ReplacementCard as Component } from './replacement_card.component'; + +export interface Props { + eprPackageName: string; +} + +/** + * A data-connected component which can query about Beats-based replacement options for a given EPR module. + */ +export const ReplacementCard = ({ eprPackageName }: Props) => { + const { findReplacementIntegrations } = useFindService(); + const integrations = useAsync(async () => { + return await findReplacementIntegrations({ shipper: 'beats', eprPackageName }); + }, [eprPackageName]); + + const { loading, value: replacements } = integrations; + + if (loading || !replacements || replacements.length === 0) { + return null; + } + + return ; +}; diff --git a/src/plugins/custom_integrations/public/index.ts b/src/plugins/custom_integrations/public/index.ts index 9e979dd6692bc..91da75c634a44 100755 --- a/src/plugins/custom_integrations/public/index.ts +++ b/src/plugins/custom_integrations/public/index.ts @@ -13,4 +13,8 @@ import { CustomIntegrationsPlugin } from './plugin'; export function plugin() { return new CustomIntegrationsPlugin(); } + export { CustomIntegrationsSetup, CustomIntegrationsStart } from './types'; + +export { withSuspense, LazyReplacementCard } from './components'; +export { filterCustomIntegrations } from './services/find'; diff --git a/src/plugins/custom_integrations/public/mocks.ts b/src/plugins/custom_integrations/public/mocks.ts index 2e6bc491c2c5c..a8fedbbb712b2 100644 --- a/src/plugins/custom_integrations/public/mocks.ts +++ b/src/plugins/custom_integrations/public/mocks.ts @@ -6,7 +6,11 @@ * Side Public License, v 1. */ -import { CustomIntegrationsSetup } from './types'; +import { pluginServices } from './services'; +import { PluginServiceRegistry } from '../../presentation_util/public'; +import { CustomIntegrationsSetup, CustomIntegrationsStart } from './types'; +import { CustomIntegrationsServices } from './services'; +import { providers } from './services/stub'; function createCustomIntegrationsSetup(): jest.Mocked { const mock: jest.Mocked = { @@ -16,6 +20,17 @@ function createCustomIntegrationsSetup(): jest.Mocked { return mock; } +function createCustomIntegrationsStart(): jest.Mocked { + const registry = new PluginServiceRegistry(providers); + pluginServices.setRegistry(registry.start({})); + const ContextProvider = pluginServices.getContextProvider(); + + return { + ContextProvider: jest.fn(ContextProvider), + }; +} + export const customIntegrationsMock = { createSetup: createCustomIntegrationsSetup, + createStart: createCustomIntegrationsStart, }; diff --git a/src/plugins/custom_integrations/public/plugin.ts b/src/plugins/custom_integrations/public/plugin.ts index 7ea7a829e8072..a3470fefba46c 100755 --- a/src/plugins/custom_integrations/public/plugin.ts +++ b/src/plugins/custom_integrations/public/plugin.ts @@ -7,13 +7,20 @@ */ import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; -import { CustomIntegrationsSetup, CustomIntegrationsStart } from './types'; +import { + CustomIntegrationsSetup, + CustomIntegrationsStart, + CustomIntegrationsStartDependencies, +} from './types'; import { CustomIntegration, ROUTES_APPEND_CUSTOM_INTEGRATIONS, ROUTES_REPLACEMENT_CUSTOM_INTEGRATIONS, } from '../common'; +import { pluginServices } from './services'; +import { pluginServiceRegistry } from './services/kibana'; + export class CustomIntegrationsPlugin implements Plugin { @@ -30,8 +37,14 @@ export class CustomIntegrationsPlugin }; } - public start(core: CoreStart): CustomIntegrationsStart { - return {}; + public start( + coreStart: CoreStart, + startPlugins: CustomIntegrationsStartDependencies + ): CustomIntegrationsStart { + pluginServices.setRegistry(pluginServiceRegistry.start({ coreStart, startPlugins })); + return { + ContextProvider: pluginServices.getContextProvider(), + }; } public stop() {} diff --git a/src/plugins/custom_integrations/public/services/find.test.ts b/src/plugins/custom_integrations/public/services/find.test.ts new file mode 100644 index 0000000000000..df52c22313b68 --- /dev/null +++ b/src/plugins/custom_integrations/public/services/find.test.ts @@ -0,0 +1,95 @@ +/* + * 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. + */ +/* + * 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 { filterCustomIntegrations } from './find'; +import { CustomIntegration } from '../../common'; + +describe('Custom Integrations Find Service', () => { + const integrations: CustomIntegration[] = [ + { + id: 'foo', + title: 'Foo', + description: 'test integration', + type: 'ui_link', + uiInternalPath: '/path/to/foo', + isBeta: false, + icons: [], + categories: ['aws', 'cloud'], + shipper: 'tests', + }, + { + id: 'bar', + title: 'Bar', + description: 'test integration', + type: 'ui_link', + uiInternalPath: '/path/to/bar', + isBeta: false, + icons: [], + categories: ['aws'], + shipper: 'other', + eprOverlap: 'eprValue', + }, + { + id: 'bar', + title: 'Bar', + description: 'test integration', + type: 'ui_link', + uiInternalPath: '/path/to/bar', + isBeta: false, + icons: [], + categories: ['cloud'], + shipper: 'other', + eprOverlap: 'eprValue', + }, + { + id: 'baz', + title: 'Baz', + description: 'test integration', + type: 'ui_link', + uiInternalPath: '/path/to/baz', + isBeta: false, + icons: [], + categories: ['cloud'], + shipper: 'tests', + eprOverlap: 'eprOtherValue', + }, + ]; + + describe('filterCustomIntegrations', () => { + test('filters on shipper', () => { + let result = filterCustomIntegrations(integrations, { shipper: 'other' }); + expect(result.length).toBe(2); + result = filterCustomIntegrations(integrations, { shipper: 'tests' }); + expect(result.length).toBe(2); + result = filterCustomIntegrations(integrations, { shipper: 'foobar' }); + expect(result.length).toBe(0); + }); + test('filters on eprOverlap', () => { + let result = filterCustomIntegrations(integrations, { eprPackageName: 'eprValue' }); + expect(result.length).toBe(2); + result = filterCustomIntegrations(integrations, { eprPackageName: 'eprOtherValue' }); + expect(result.length).toBe(1); + result = filterCustomIntegrations(integrations, { eprPackageName: 'otherValue' }); + expect(result.length).toBe(0); + }); + test('filters on categories and shipper, eprOverlap', () => { + const result = filterCustomIntegrations(integrations, { + shipper: 'other', + eprPackageName: 'eprValue', + }); + expect(result.length).toBe(2); + }); + }); +}); diff --git a/src/plugins/custom_integrations/public/services/find.ts b/src/plugins/custom_integrations/public/services/find.ts new file mode 100644 index 0000000000000..4e69327c351b4 --- /dev/null +++ b/src/plugins/custom_integrations/public/services/find.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { CustomIntegration } from '../../common'; + +interface FindParams { + eprPackageName?: string; + shipper?: string; +} + +/** + * A plugin service that finds and returns custom integrations. + */ +export interface CustomIntegrationsFindService { + findReplacementIntegrations(params?: FindParams): Promise; + findAppendedIntegrations(params?: FindParams): Promise; +} + +/** + * Filter a set of integrations by eprPackageName, and/or shipper. + */ +export const filterCustomIntegrations = ( + integrations: CustomIntegration[], + { eprPackageName, shipper }: FindParams = {} +) => { + if (!eprPackageName && !shipper) { + return integrations; + } + + let result = integrations; + + if (eprPackageName) { + result = result.filter((integration) => integration.eprOverlap === eprPackageName); + } + + if (shipper) { + result = result.filter((integration) => integration.shipper === shipper); + } + + return result; +}; diff --git a/src/plugins/custom_integrations/public/services/index.ts b/src/plugins/custom_integrations/public/services/index.ts new file mode 100644 index 0000000000000..8a257ee1a2cd7 --- /dev/null +++ b/src/plugins/custom_integrations/public/services/index.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { PluginServices } from '../../../presentation_util/public'; + +import { CustomIntegrationsFindService } from './find'; +import { CustomIntegrationsPlatformService } from './platform'; + +/** + * Services used by the custom integrations plugin. + */ +export interface CustomIntegrationsServices { + find: CustomIntegrationsFindService; + platform: CustomIntegrationsPlatformService; +} + +/** + * The `PluginServices` object for the custom integrations plugin. + * @see /src/plugins/presentation_util/public/services/create/index.ts + */ +export const pluginServices = new PluginServices(); + +/** + * A React hook that provides connections to the `CustomIntegrationsFindService`. + */ +export const useFindService = () => (() => pluginServices.getHooks().find.useService())(); + +/** + * A React hook that provides connections to the `CustomIntegrationsPlatformService`. + */ +export const usePlatformService = () => (() => pluginServices.getHooks().platform.useService())(); diff --git a/src/plugins/custom_integrations/public/services/kibana/find.ts b/src/plugins/custom_integrations/public/services/kibana/find.ts new file mode 100644 index 0000000000000..5fc7626baa1e1 --- /dev/null +++ b/src/plugins/custom_integrations/public/services/kibana/find.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { + CustomIntegration, + ROUTES_APPEND_CUSTOM_INTEGRATIONS, + ROUTES_REPLACEMENT_CUSTOM_INTEGRATIONS, +} from '../../../common'; +import { KibanaPluginServiceFactory } from '../../../../presentation_util/public'; + +import { CustomIntegrationsStartDependencies } from '../../types'; +import { CustomIntegrationsFindService, filterCustomIntegrations } from '../find'; + +/** + * A type definition for a factory to produce the `CustomIntegrationsFindService` for use in Kibana. + * @see /src/plugins/presentation_util/public/services/create/factory.ts + */ +export type CustomIntegrationsFindServiceFactory = KibanaPluginServiceFactory< + CustomIntegrationsFindService, + CustomIntegrationsStartDependencies +>; + +/** + * A factory to produce the `CustomIntegrationsFindService` for use in Kibana. + */ +export const findServiceFactory: CustomIntegrationsFindServiceFactory = ({ coreStart }) => ({ + findAppendedIntegrations: async (params) => { + const integrations: CustomIntegration[] = await coreStart.http.get( + ROUTES_APPEND_CUSTOM_INTEGRATIONS + ); + + return filterCustomIntegrations(integrations, params); + }, + findReplacementIntegrations: async (params) => { + const replacements: CustomIntegration[] = await coreStart.http.get( + ROUTES_REPLACEMENT_CUSTOM_INTEGRATIONS + ); + + return filterCustomIntegrations(replacements, params); + }, +}); diff --git a/src/plugins/custom_integrations/public/services/kibana/index.ts b/src/plugins/custom_integrations/public/services/kibana/index.ts new file mode 100644 index 0000000000000..d3cf27b9bc7c0 --- /dev/null +++ b/src/plugins/custom_integrations/public/services/kibana/index.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { + PluginServiceProviders, + PluginServiceProvider, + PluginServiceRegistry, + KibanaPluginServiceParams, +} from '../../../../presentation_util/public'; + +import { CustomIntegrationsServices } from '..'; +import { CustomIntegrationsStartDependencies } from '../../types'; + +import { findServiceFactory } from './find'; +import { platformServiceFactory } from './platform'; + +export { findServiceFactory } from './find'; +export { platformServiceFactory } from './platform'; + +/** + * A set of `PluginServiceProvider`s for use in Kibana. + * @see /src/plugins/presentation_util/public/services/create/provider.tsx + */ +export const pluginServiceProviders: PluginServiceProviders< + CustomIntegrationsServices, + KibanaPluginServiceParams +> = { + find: new PluginServiceProvider(findServiceFactory), + platform: new PluginServiceProvider(platformServiceFactory), +}; + +/** + * A `PluginServiceRegistry` for use in Kibana. + * @see /src/plugins/presentation_util/public/services/create/registry.tsx + */ +export const pluginServiceRegistry = new PluginServiceRegistry< + CustomIntegrationsServices, + KibanaPluginServiceParams +>(pluginServiceProviders); diff --git a/src/plugins/custom_integrations/public/services/kibana/platform.ts b/src/plugins/custom_integrations/public/services/kibana/platform.ts new file mode 100644 index 0000000000000..e6fe89b68c975 --- /dev/null +++ b/src/plugins/custom_integrations/public/services/kibana/platform.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { KibanaPluginServiceFactory } from '../../../../presentation_util/public'; + +import type { CustomIntegrationsPlatformService } from '../platform'; +import type { CustomIntegrationsStartDependencies } from '../../types'; + +/** + * A type definition for a factory to produce the `CustomIntegrationsPlatformService` for use in Kibana. + * @see /src/plugins/presentation_util/public/services/create/factory.ts + */ +export type CustomIntegrationsPlatformServiceFactory = KibanaPluginServiceFactory< + CustomIntegrationsPlatformService, + CustomIntegrationsStartDependencies +>; + +/** + * A factory to produce the `CustomIntegrationsPlatformService` for use in Kibana. + */ +export const platformServiceFactory: CustomIntegrationsPlatformServiceFactory = ({ + coreStart, +}) => ({ + getBasePath: coreStart.http.basePath.get, + getAbsolutePath: (path: string): string => coreStart.http.basePath.prepend(`${path}`), +}); diff --git a/src/plugins/custom_integrations/public/services/platform.ts b/src/plugins/custom_integrations/public/services/platform.ts new file mode 100644 index 0000000000000..0eb9c7d5c3c10 --- /dev/null +++ b/src/plugins/custom_integrations/public/services/platform.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 interface CustomIntegrationsPlatformService { + getBasePath: () => string; + getAbsolutePath: (path: string) => string; +} diff --git a/src/plugins/custom_integrations/public/services/storybook/index.ts b/src/plugins/custom_integrations/public/services/storybook/index.ts new file mode 100644 index 0000000000000..4dfed1b37e294 --- /dev/null +++ b/src/plugins/custom_integrations/public/services/storybook/index.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 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 { + PluginServiceProviders, + PluginServiceProvider, + PluginServiceRegistry, +} from '../../../../presentation_util/public'; + +import { CustomIntegrationsServices } from '..'; +import { findServiceFactory } from '../stub/find'; +import { platformServiceFactory } from '../stub/platform'; + +export { findServiceFactory } from '../stub/find'; +export { platformServiceFactory } from '../stub/platform'; + +/** + * A set of `PluginServiceProvider`s for use in Storybook. + * @see /src/plugins/presentation_util/public/services/create/provider.tsx + */ +export const providers: PluginServiceProviders = { + find: new PluginServiceProvider(findServiceFactory), + platform: new PluginServiceProvider(platformServiceFactory), +}; + +/** + * A `PluginServiceRegistry` for use in Storybook. + * @see /src/plugins/presentation_util/public/services/create/registry.tsx + */ +export const registry = new PluginServiceRegistry(providers); diff --git a/src/plugins/custom_integrations/public/services/stub/find.ts b/src/plugins/custom_integrations/public/services/stub/find.ts new file mode 100644 index 0000000000000..08def4e63471d --- /dev/null +++ b/src/plugins/custom_integrations/public/services/stub/find.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginServiceFactory } from '../../../../presentation_util/public'; + +import { CustomIntegrationsFindService, filterCustomIntegrations } from '../find'; + +/** + * A type definition for a factory to produce the `CustomIntegrationsFindService` with stubbed output. + * @see /src/plugins/presentation_util/public/services/create/factory.ts + */ +export type CustomIntegrationsFindServiceFactory = + PluginServiceFactory; + +/** + * A factory to produce the `CustomIntegrationsFindService` with stubbed output. + */ +export const findServiceFactory: CustomIntegrationsFindServiceFactory = () => ({ + findAppendedIntegrations: async (params) => { + const { integrations } = await import('./fixtures/integrations'); + return filterCustomIntegrations(integrations, params); + }, + findReplacementIntegrations: async (params) => { + const { integrations } = await import('./fixtures/integrations'); + return filterCustomIntegrations(integrations, params); + }, +}); diff --git a/src/plugins/custom_integrations/public/services/stub/fixtures/integrations.ts b/src/plugins/custom_integrations/public/services/stub/fixtures/integrations.ts new file mode 100644 index 0000000000000..7553deada9e26 --- /dev/null +++ b/src/plugins/custom_integrations/public/services/stub/fixtures/integrations.ts @@ -0,0 +1,1884 @@ +/* + * 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 { CustomIntegration } from '../../../../common'; + +export const integrations: CustomIntegration[] = [ + { + type: 'ui_link', + id: 'System logs', + title: 'System logs', + categories: ['os_system', 'security'], + uiInternalPath: '/app/home#/tutorial/systemLogs', + description: 'Collect system logs of common Unix/Linux based distributions.', + icons: [ + { + type: 'eui', + src: 'logoLogging', + }, + ], + shipper: 'beats', + eprOverlap: 'system', + isBeta: false, + }, + { + type: 'ui_link', + id: 'System metrics', + title: 'System metrics', + categories: ['os_system', 'security'], + uiInternalPath: '/app/home#/tutorial/systemMetrics', + description: 'Collect CPU, memory, network, and disk statistics from the host.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/system.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'system', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Apache logs', + title: 'Apache logs', + categories: ['web'], + uiInternalPath: '/app/home#/tutorial/apacheLogs', + description: 'Collect and parse access and error logs created by the Apache HTTP server.', + icons: [ + { + type: 'eui', + src: 'logoApache', + }, + ], + shipper: 'beats', + eprOverlap: 'apache', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Apache metrics', + title: 'Apache metrics', + categories: ['web'], + uiInternalPath: '/app/home#/tutorial/apacheMetrics', + description: 'Fetch internal metrics from the Apache 2 HTTP server.', + icons: [ + { + type: 'eui', + src: 'logoApache', + }, + ], + shipper: 'beats', + eprOverlap: 'apache', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Elasticsearch logs', + title: 'Elasticsearch logs', + categories: ['containers', 'os_system'], + uiInternalPath: '/app/home#/tutorial/elasticsearchLogs', + description: 'Collect and parse logs created by Elasticsearch.', + icons: [ + { + type: 'eui', + src: 'logoElasticsearch', + }, + ], + shipper: 'beats', + eprOverlap: 'elasticsearch', + isBeta: false, + }, + { + type: 'ui_link', + id: 'IIS logs', + title: 'IIS logs', + categories: ['web'], + uiInternalPath: '/app/home#/tutorial/iisLogs', + description: 'Collect and parse access and error logs created by the IIS HTTP server.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/iis.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'iis', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Kafka logs', + title: 'Kafka logs', + categories: ['message_queue'], + uiInternalPath: '/app/home#/tutorial/kafkaLogs', + description: 'Collect and parse logs created by Kafka.', + icons: [ + { + type: 'eui', + src: 'logoKafka', + }, + ], + shipper: 'beats', + eprOverlap: 'kafka', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Logstash logs', + title: 'Logstash logs', + categories: ['custom'], + uiInternalPath: '/app/home#/tutorial/logstashLogs', + description: 'Collect Logstash main and slow logs.', + icons: [ + { + type: 'eui', + src: 'logoLogstash', + }, + ], + shipper: 'beats', + eprOverlap: 'logstash', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Nginx logs', + title: 'Nginx logs', + categories: ['web', 'security'], + uiInternalPath: '/app/home#/tutorial/nginxLogs', + description: 'Collect and parse access and error logs created by the Nginx HTTP server.', + icons: [ + { + type: 'eui', + src: 'logoNginx', + }, + ], + shipper: 'beats', + eprOverlap: 'nginx', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Nginx metrics', + title: 'Nginx metrics', + categories: ['web', 'security'], + uiInternalPath: '/app/home#/tutorial/nginxMetrics', + description: 'Fetch internal metrics from the Nginx HTTP server.', + icons: [ + { + type: 'eui', + src: 'logoNginx', + }, + ], + shipper: 'beats', + eprOverlap: 'nginx', + isBeta: false, + }, + { + type: 'ui_link', + id: 'MySQL logs', + title: 'MySQL logs', + categories: ['datastore'], + uiInternalPath: '/app/home#/tutorial/mysqlLogs', + description: 'Collect and parse error and slow logs created by MySQL.', + icons: [ + { + type: 'eui', + src: 'logoMySQL', + }, + ], + shipper: 'beats', + eprOverlap: 'mysql', + isBeta: false, + }, + { + type: 'ui_link', + id: 'MySQL metrics', + title: 'MySQL metrics', + categories: ['datastore'], + uiInternalPath: '/app/home#/tutorial/mysqlMetrics', + description: 'Fetch internal metrics from MySQL.', + icons: [ + { + type: 'eui', + src: 'logoMySQL', + }, + ], + shipper: 'beats', + eprOverlap: 'mysql', + isBeta: false, + }, + { + type: 'ui_link', + id: 'MongoDB metrics', + title: 'MongoDB metrics', + categories: ['datastore'], + uiInternalPath: '/app/home#/tutorial/mongodbMetrics', + description: 'Fetch internal metrics from MongoDB.', + icons: [ + { + type: 'eui', + src: 'logoMongodb', + }, + ], + shipper: 'beats', + eprOverlap: 'mongodb', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Osquery logs', + title: 'Osquery logs', + categories: ['security', 'os_system'], + uiInternalPath: '/app/home#/tutorial/osqueryLogs', + description: 'Collect osquery logs in JSON format.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/osquery.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'osquery', + isBeta: false, + }, + { + type: 'ui_link', + id: 'PHP-FPM metrics', + title: 'PHP-FPM metrics', + categories: ['security'], + uiInternalPath: '/app/home#/tutorial/phpfpmMetrics', + description: 'Fetch internal metrics from PHP-FPM.', + icons: [ + { + type: 'eui', + src: 'logoPhp', + }, + ], + shipper: 'beats', + eprOverlap: 'php_fpm', + isBeta: false, + }, + { + type: 'ui_link', + id: 'PostgreSQL metrics', + title: 'PostgreSQL metrics', + categories: ['datastore'], + uiInternalPath: '/app/home#/tutorial/postgresqlMetrics', + description: 'Fetch internal metrics from PostgreSQL.', + icons: [ + { + type: 'eui', + src: 'logoPostgres', + }, + ], + shipper: 'beats', + eprOverlap: 'postgresql', + isBeta: false, + }, + { + type: 'ui_link', + id: 'PostgreSQL logs', + title: 'PostgreSQL logs', + categories: ['datastore'], + uiInternalPath: '/app/home#/tutorial/postgresqlLogs', + description: 'Collect and parse error and slow logs created by PostgreSQL.', + icons: [ + { + type: 'eui', + src: 'logoPostgres', + }, + ], + shipper: 'beats', + eprOverlap: 'postgresql', + isBeta: false, + }, + { + type: 'ui_link', + id: 'RabbitMQ metrics', + title: 'RabbitMQ metrics', + categories: ['message_queue'], + uiInternalPath: '/app/home#/tutorial/rabbitmqMetrics', + description: 'Fetch internal metrics from the RabbitMQ server.', + icons: [ + { + type: 'eui', + src: 'logoRabbitmq', + }, + ], + shipper: 'beats', + eprOverlap: 'rabbitmq', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Redis logs', + title: 'Redis logs', + categories: ['datastore', 'message_queue'], + uiInternalPath: '/app/home#/tutorial/redisLogs', + description: 'Collect and parse error and slow logs created by Redis.', + icons: [ + { + type: 'eui', + src: 'logoRedis', + }, + ], + shipper: 'beats', + eprOverlap: 'redis', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Redis metrics', + title: 'Redis metrics', + categories: ['datastore', 'message_queue'], + uiInternalPath: '/app/home#/tutorial/redisMetrics', + description: 'Fetch internal metrics from Redis.', + icons: [ + { + type: 'eui', + src: 'logoRedis', + }, + ], + shipper: 'beats', + eprOverlap: 'redis', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Suricata logs', + title: 'Suricata logs', + categories: ['network', 'security'], + uiInternalPath: '/app/home#/tutorial/suricataLogs', + description: 'Collect Suricata IDS/IPS/NSM logs.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/suricata.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'suricata', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Docker metrics', + title: 'Docker metrics', + categories: ['containers', 'os_system'], + uiInternalPath: '/app/home#/tutorial/dockerMetrics', + description: 'Fetch metrics about your Docker containers.', + icons: [ + { + type: 'eui', + src: 'logoDocker', + }, + ], + shipper: 'beats', + eprOverlap: 'docker', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Kubernetes metrics', + title: 'Kubernetes metrics', + categories: ['containers', 'kubernetes'], + uiInternalPath: '/app/home#/tutorial/kubernetesMetrics', + description: 'Fetch metrics from your Kubernetes installation.', + icons: [ + { + type: 'eui', + src: 'logoKubernetes', + }, + ], + shipper: 'beats', + eprOverlap: 'kubernetes', + isBeta: false, + }, + { + type: 'ui_link', + id: 'uWSGI metrics', + title: 'uWSGI metrics', + categories: ['web', 'security'], + uiInternalPath: '/app/home#/tutorial/uwsgiMetrics', + description: 'Fetch internal metrics from the uWSGI server.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/uwsgi.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'uwsgi', + isBeta: false, + }, + { + type: 'ui_link', + id: 'NetFlow / IPFIX Collector', + title: 'NetFlow / IPFIX Collector', + categories: ['network', 'security'], + uiInternalPath: '/app/home#/tutorial/netflowLogs', + description: 'Collect NetFlow and IPFIX flow records.', + icons: [ + { + type: 'eui', + src: 'logoBeats', + }, + ], + shipper: 'beats', + eprOverlap: 'netflow', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Traefik logs', + title: 'Traefik logs', + categories: ['web', 'security'], + uiInternalPath: '/app/home#/tutorial/traefikLogs', + description: 'Collect Traefik access logs.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/traefik.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'traefik', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Ceph metrics', + title: 'Ceph metrics', + categories: ['network', 'security'], + uiInternalPath: '/app/home#/tutorial/cephMetrics', + description: 'Fetch internal metrics from the Ceph server.', + icons: [ + { + type: 'eui', + src: 'logoCeph', + }, + ], + shipper: 'beats', + eprOverlap: 'ceph', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Aerospike metrics', + title: 'Aerospike metrics', + categories: ['web'], + uiInternalPath: '/app/home#/tutorial/aerospikeMetrics', + description: 'Fetch internal metrics from the Aerospike server.', + icons: [ + { + type: 'eui', + src: 'logoAerospike', + }, + ], + shipper: 'beats', + eprOverlap: 'aerospike', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Couchbase metrics', + title: 'Couchbase metrics', + categories: ['security', 'network', 'web'], + uiInternalPath: '/app/home#/tutorial/couchbaseMetrics', + description: 'Fetch internal metrics from Couchbase.', + icons: [ + { + type: 'eui', + src: 'logoCouchbase', + }, + ], + shipper: 'beats', + eprOverlap: 'couchbase', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Dropwizard metrics', + title: 'Dropwizard metrics', + categories: ['elastic_stack', 'datastore'], + uiInternalPath: '/app/home#/tutorial/dropwizardMetrics', + description: 'Fetch internal metrics from Dropwizard Java application.', + icons: [ + { + type: 'eui', + src: 'logoDropwizard', + }, + ], + shipper: 'beats', + eprOverlap: 'dropwizard', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Elasticsearch metrics', + title: 'Elasticsearch metrics', + categories: ['elastic_stack', 'datastore'], + uiInternalPath: '/app/home#/tutorial/elasticsearchMetrics', + description: 'Fetch internal metrics from Elasticsearch.', + icons: [ + { + type: 'eui', + src: 'logoElasticsearch', + }, + ], + shipper: 'beats', + eprOverlap: 'elasticsearch', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Etcd metrics', + title: 'Etcd metrics', + categories: ['elastic_stack', 'datastore'], + uiInternalPath: '/app/home#/tutorial/etcdMetrics', + description: 'Fetch internal metrics from the Etcd server.', + icons: [ + { + type: 'eui', + src: 'logoEtcd', + }, + ], + shipper: 'beats', + eprOverlap: 'etcd', + isBeta: false, + }, + { + type: 'ui_link', + id: 'HAProxy metrics', + title: 'HAProxy metrics', + categories: ['network', 'web'], + uiInternalPath: '/app/home#/tutorial/haproxyMetrics', + description: 'Fetch internal metrics from the HAProxy server.', + icons: [ + { + type: 'eui', + src: 'logoHAproxy', + }, + ], + shipper: 'beats', + eprOverlap: 'haproxy', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Kafka metrics', + title: 'Kafka metrics', + categories: ['message_queue'], + uiInternalPath: '/app/home#/tutorial/kafkaMetrics', + description: 'Fetch internal metrics from the Kafka server.', + icons: [ + { + type: 'eui', + src: 'logoKafka', + }, + ], + shipper: 'beats', + eprOverlap: 'kafka', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Kibana metrics', + title: 'Kibana metrics', + categories: ['message_queue'], + uiInternalPath: '/app/home#/tutorial/kibanaMetrics', + description: 'Fetch internal metrics from Kibana.', + icons: [ + { + type: 'eui', + src: 'logoKibana', + }, + ], + shipper: 'beats', + eprOverlap: 'kibana', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Memcached metrics', + title: 'Memcached metrics', + categories: ['custom'], + uiInternalPath: '/app/home#/tutorial/memcachedMetrics', + description: 'Fetch internal metrics from the Memcached server.', + icons: [ + { + type: 'eui', + src: 'logoMemcached', + }, + ], + shipper: 'beats', + eprOverlap: 'memcached', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Munin metrics', + title: 'Munin metrics', + categories: ['datastore'], + uiInternalPath: '/app/home#/tutorial/muninMetrics', + description: 'Fetch internal metrics from the Munin server.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/munin.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'munin', + isBeta: false, + }, + { + type: 'ui_link', + id: 'vSphere metrics', + title: 'vSphere metrics', + categories: ['web', 'security'], + uiInternalPath: '/app/home#/tutorial/vsphereMetrics', + description: 'Fetch internal metrics from vSphere.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/vsphere.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'vsphere', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Windows metrics', + title: 'Windows metrics', + categories: ['os_system', 'security'], + uiInternalPath: '/app/home#/tutorial/windowsMetrics', + description: 'Fetch internal metrics from Windows.', + icons: [ + { + type: 'eui', + src: 'logoWindows', + }, + ], + shipper: 'beats', + eprOverlap: 'windows', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Windows Event Log', + title: 'Windows Event Log', + categories: ['os_system', 'security'], + uiInternalPath: '/app/home#/tutorial/windowsEventLogs', + description: 'Fetch logs from the Windows Event Log.', + icons: [ + { + type: 'eui', + src: 'logoWindows', + }, + ], + shipper: 'beats', + eprOverlap: 'windows', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Golang metrics', + title: 'Golang metrics', + categories: ['google_cloud', 'cloud', 'network', 'security'], + uiInternalPath: '/app/home#/tutorial/golangMetrics', + description: 'Fetch internal metrics from a Golang app.', + icons: [ + { + type: 'eui', + src: 'logoGolang', + }, + ], + shipper: 'beats', + eprOverlap: 'golang', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Logstash metrics', + title: 'Logstash metrics', + categories: ['custom'], + uiInternalPath: '/app/home#/tutorial/logstashMetrics', + description: 'Fetch internal metrics from a Logstash server.', + icons: [ + { + type: 'eui', + src: 'logoLogstash', + }, + ], + shipper: 'beats', + eprOverlap: 'logstash', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Prometheus metrics', + title: 'Prometheus metrics', + categories: ['monitoring', 'datastore'], + uiInternalPath: '/app/home#/tutorial/prometheusMetrics', + description: 'Fetch metrics from a Prometheus exporter.', + icons: [ + { + type: 'eui', + src: 'logoPrometheus', + }, + ], + shipper: 'beats', + eprOverlap: 'prometheus', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Zookeeper metrics', + title: 'Zookeeper metrics', + categories: ['datastore', 'config_management'], + uiInternalPath: '/app/home#/tutorial/zookeeperMetrics', + description: 'Fetch internal metrics from a Zookeeper server.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/zookeeper.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'zookeeper', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Uptime Monitors', + title: 'Uptime Monitors', + categories: ['web', 'security'], + uiInternalPath: '/app/home#/tutorial/uptimeMonitors', + description: 'Monitor services for their availability', + icons: [ + { + type: 'eui', + src: 'uptimeApp', + }, + ], + shipper: 'beats', + eprOverlap: 'uptime', + isBeta: false, + }, + { + type: 'ui_link', + id: 'AWS Cloudwatch logs', + title: 'AWS Cloudwatch logs', + categories: ['security', 'network', 'web'], + uiInternalPath: '/app/home#/tutorial/cloudwatchLogs', + description: 'Collect Cloudwatch logs with Functionbeat.', + icons: [ + { + type: 'eui', + src: 'logoAWS', + }, + ], + shipper: 'beats', + eprOverlap: 'aws', + isBeta: false, + }, + { + type: 'ui_link', + id: 'AWS metrics', + title: 'AWS metrics', + categories: ['aws', 'cloud', 'datastore', 'security', 'network'], + uiInternalPath: '/app/home#/tutorial/awsMetrics', + description: 'Fetch monitoring metrics for EC2 instances from the AWS APIs and Cloudwatch.', + icons: [ + { + type: 'eui', + src: 'logoAWS', + }, + ], + shipper: 'beats', + eprOverlap: 'aws', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Microsoft SQL Server Metrics', + title: 'Microsoft SQL Server Metrics', + categories: ['datastore'], + uiInternalPath: '/app/home#/tutorial/mssqlMetrics', + description: 'Fetch monitoring metrics from a Microsoft SQL Server instance', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/mssql.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'mssql', + isBeta: false, + }, + { + type: 'ui_link', + id: 'NATS metrics', + title: 'NATS metrics', + categories: ['message_queue'], + uiInternalPath: '/app/home#/tutorial/natsMetrics', + description: 'Fetch monitoring metrics from the Nats server.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/nats.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'nats', + isBeta: false, + }, + { + type: 'ui_link', + id: 'NATS logs', + title: 'NATS logs', + categories: ['message_queue'], + uiInternalPath: '/app/home#/tutorial/natsLogs', + description: 'Collect and parse logs created by Nats.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/nats.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'nats', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Zeek logs', + title: 'Zeek logs', + categories: ['network', 'monitoring', 'security'], + uiInternalPath: '/app/home#/tutorial/zeekLogs', + description: 'Collect Zeek network security monitoring logs.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/zeek.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'zeek', + isBeta: false, + }, + { + type: 'ui_link', + id: 'CoreDNS metrics', + title: 'CoreDNS metrics', + categories: ['security', 'network', 'web'], + uiInternalPath: '/app/home#/tutorial/corednsMetrics', + description: 'Fetch monitoring metrics from the CoreDNS server.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/coredns.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'coredns', + isBeta: false, + }, + { + type: 'ui_link', + id: 'CoreDNS logs', + title: 'CoreDNS logs', + categories: ['security', 'network', 'web'], + uiInternalPath: '/app/home#/tutorial/corednsLogs', + description: 'Collect CoreDNS logs.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/coredns.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'coredns', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Auditbeat', + title: 'Auditbeat', + categories: ['web'], + uiInternalPath: '/app/home#/tutorial/auditbeat', + description: 'Collect audit data from your hosts.', + icons: [ + { + type: 'eui', + src: 'securityAnalyticsApp', + }, + ], + shipper: 'beats', + eprOverlap: 'auditbeat', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Iptables logs', + title: 'Iptables logs', + categories: ['network', 'security'], + uiInternalPath: '/app/home#/tutorial/iptablesLogs', + description: 'Collect iptables and ip6tables logs.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/linux.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'iptables', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Cisco logs', + title: 'Cisco logs', + categories: ['network', 'security'], + uiInternalPath: '/app/home#/tutorial/ciscoLogs', + description: 'Collect Cisco network device logs over syslog or from a file.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/cisco.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'cisco', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Envoy Proxy logs', + title: 'Envoy Proxy logs', + categories: ['elastic_stack', 'datastore'], + uiInternalPath: '/app/home#/tutorial/envoyproxyLogs', + description: 'Collect Envoy Proxy logs.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/envoyproxy.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'envoyproxy', + isBeta: false, + }, + { + type: 'ui_link', + id: 'CouchDB metrics', + title: 'CouchDB metrics', + categories: ['security', 'network', 'web'], + uiInternalPath: '/app/home#/tutorial/couchdbMetrics', + description: 'Fetch monitoring metrics from the CouchdB server.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/couchdb.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'couchdb', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Consul metrics', + title: 'Consul metrics', + categories: ['security', 'network', 'web'], + uiInternalPath: '/app/home#/tutorial/consulMetrics', + description: 'Fetch monitoring metrics from the Consul server.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/consul.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'consul', + isBeta: false, + }, + { + type: 'ui_link', + id: 'CockroachDB metrics', + title: 'CockroachDB metrics', + categories: ['security', 'network', 'web'], + uiInternalPath: '/app/home#/tutorial/cockroachdbMetrics', + description: 'Fetch monitoring metrics from the CockroachDB server.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/cockroachdb.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'cockroachdb', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Traefik metrics', + title: 'Traefik metrics', + categories: ['web', 'security'], + uiInternalPath: '/app/home#/tutorial/traefikMetrics', + description: 'Fetch monitoring metrics from Traefik.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/traefik.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'traefik', + isBeta: false, + }, + { + type: 'ui_link', + id: 'AWS S3 based logs', + title: 'AWS S3 based logs', + categories: ['aws', 'cloud', 'datastore', 'security', 'network'], + uiInternalPath: '/app/home#/tutorial/awsLogs', + description: 'Collect AWS logs from S3 bucket with Filebeat.', + icons: [ + { + type: 'eui', + src: 'logoAWS', + }, + ], + shipper: 'beats', + eprOverlap: 'aws', + isBeta: false, + }, + { + type: 'ui_link', + id: 'ActiveMQ logs', + title: 'ActiveMQ logs', + categories: ['web'], + uiInternalPath: '/app/home#/tutorial/activemqLogs', + description: 'Collect ActiveMQ logs with Filebeat.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/activemq.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'activemq', + isBeta: false, + }, + { + type: 'ui_link', + id: 'ActiveMQ metrics', + title: 'ActiveMQ metrics', + categories: ['web'], + uiInternalPath: '/app/home#/tutorial/activemqMetrics', + description: 'Fetch monitoring metrics from ActiveMQ instances.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/activemq.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'activemq', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Azure metrics', + title: 'Azure metrics', + categories: ['azure', 'cloud', 'network', 'security'], + uiInternalPath: '/app/home#/tutorial/azureMetrics', + description: 'Fetch Azure Monitor metrics.', + icons: [ + { + type: 'eui', + src: 'logoAzure', + }, + ], + shipper: 'beats', + eprOverlap: 'azure', + isBeta: false, + }, + { + type: 'ui_link', + id: 'IBM MQ logs', + title: 'IBM MQ logs', + categories: ['security'], + uiInternalPath: '/app/home#/tutorial/ibmmqLogs', + description: 'Collect IBM MQ logs with Filebeat.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/ibmmq.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'ibmmq', + isBeta: false, + }, + { + type: 'ui_link', + id: 'IBM MQ metrics', + title: 'IBM MQ metrics', + categories: ['security'], + uiInternalPath: '/app/home#/tutorial/ibmmqMetrics', + description: 'Fetch monitoring metrics from IBM MQ instances.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/ibmmq.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'ibmmq', + isBeta: false, + }, + { + type: 'ui_link', + id: 'STAN metrics', + title: 'STAN metrics', + categories: ['message_queue', 'kubernetes'], + uiInternalPath: '/app/home#/tutorial/stanMetrics', + description: 'Fetch monitoring metrics from the STAN server.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/stan.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'stan', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Envoy Proxy metrics', + title: 'Envoy Proxy metrics', + categories: ['elastic_stack', 'datastore'], + uiInternalPath: '/app/home#/tutorial/envoyproxyMetrics', + description: 'Fetch monitoring metrics from Envoy Proxy.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/envoyproxy.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'envoyproxy', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Statsd metrics', + title: 'Statsd metrics', + categories: ['message_queue', 'kubernetes'], + uiInternalPath: '/app/home#/tutorial/statsdMetrics', + description: 'Fetch monitoring metrics from statsd.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/statsd.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'statsd', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Redis Enterprise metrics', + title: 'Redis Enterprise metrics', + categories: ['datastore', 'message_queue'], + uiInternalPath: '/app/home#/tutorial/redisenterpriseMetrics', + description: 'Fetch monitoring metrics from Redis Enterprise Server.', + icons: [ + { + type: 'eui', + src: 'logoRedis', + }, + ], + shipper: 'beats', + eprOverlap: 'redisenterprise', + isBeta: false, + }, + { + type: 'ui_link', + id: 'OpenMetrics metrics', + title: 'OpenMetrics metrics', + categories: ['security'], + uiInternalPath: '/app/home#/tutorial/openmetricsMetrics', + description: 'Fetch metrics from an endpoint that serves metrics in OpenMetrics format.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/openmetrics.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'openmetrics', + isBeta: false, + }, + { + type: 'ui_link', + id: 'oracle metrics', + title: 'oracle metrics', + categories: ['security'], + uiInternalPath: '/app/home#/tutorial/oracleMetrics', + description: 'Fetch internal metrics from a Oracle server.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/oracle.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'oracle', + isBeta: false, + }, + { + type: 'ui_link', + id: 'IIS Metrics', + title: 'IIS Metrics', + categories: ['web'], + uiInternalPath: '/app/home#/tutorial/iisMetrics', + description: 'Collect IIS server related metrics.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/iis.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'iis', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Azure logs', + title: 'Azure logs', + categories: ['azure', 'cloud', 'network', 'security'], + uiInternalPath: '/app/home#/tutorial/azureLogs', + description: 'Collects Azure activity and audit related logs.', + icons: [ + { + type: 'eui', + src: 'logoAzure', + }, + ], + shipper: 'beats', + eprOverlap: 'azure', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Google Cloud metrics', + title: 'Google Cloud metrics', + categories: ['google_cloud', 'cloud', 'network', 'security'], + uiInternalPath: '/app/home#/tutorial/gcpMetrics', + description: + 'Fetch monitoring metrics from Google Cloud Platform using Stackdriver Monitoring API.', + icons: [ + { + type: 'eui', + src: 'logoGCP', + }, + ], + shipper: 'beats', + eprOverlap: 'gcp', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Auditd logs', + title: 'Auditd logs', + categories: ['os_system'], + uiInternalPath: '/app/home#/tutorial/auditdLogs', + description: 'Collect logs from the Linux auditd daemon.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/linux.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'auditd', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Barracuda logs', + title: 'Barracuda logs', + categories: ['network', 'security'], + uiInternalPath: '/app/home#/tutorial/barracudaLogs', + description: 'Collect Barracuda Web Application Firewall logs over syslog or from a file.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/barracuda.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'barracuda', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Bluecoat logs', + title: 'Bluecoat logs', + categories: ['network', 'security'], + uiInternalPath: '/app/home#/tutorial/bluecoatLogs', + description: 'Collect Blue Coat Director logs over syslog or from a file.', + icons: [ + { + type: 'eui', + src: 'logoLogging', + }, + ], + shipper: 'beats', + eprOverlap: 'bluecoat', + isBeta: false, + }, + { + type: 'ui_link', + id: 'CEF logs', + title: 'CEF logs', + categories: ['network', 'security'], + uiInternalPath: '/app/home#/tutorial/cefLogs', + description: 'Collect Common Event Format (CEF) log data over syslog.', + icons: [ + { + type: 'eui', + src: 'logoLogging', + }, + ], + shipper: 'beats', + eprOverlap: 'cef', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Check Point logs', + title: 'Check Point logs', + categories: ['security'], + uiInternalPath: '/app/home#/tutorial/checkpointLogs', + description: 'Collect Check Point firewall logs.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/checkpoint.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'checkpoint', + isBeta: false, + }, + { + type: 'ui_link', + id: 'CrowdStrike logs', + title: 'CrowdStrike logs', + categories: ['security'], + uiInternalPath: '/app/home#/tutorial/crowdstrikeLogs', + description: 'Collect CrowdStrike Falcon logs using the Falcon SIEM Connector.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/crowdstrike.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'crowdstrike', + isBeta: false, + }, + { + type: 'ui_link', + id: 'CylancePROTECT logs', + title: 'CylancePROTECT logs', + categories: ['security'], + uiInternalPath: '/app/home#/tutorial/cylanceLogs', + description: 'Collect CylancePROTECT logs over syslog or from a file.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/cylance.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'cylance', + isBeta: false, + }, + { + type: 'ui_link', + id: 'F5 logs', + title: 'F5 logs', + categories: ['network', 'security'], + uiInternalPath: '/app/home#/tutorial/f5Logs', + description: 'Collect F5 Big-IP Access Policy Manager logs over syslog or from a file.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/f5.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'f5', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Fortinet logs', + title: 'Fortinet logs', + categories: ['security'], + uiInternalPath: '/app/home#/tutorial/fortinetLogs', + description: 'Collect Fortinet FortiOS logs over syslog.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/fortinet.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'fortinet', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Google Cloud logs', + title: 'Google Cloud logs', + categories: ['google_cloud', 'cloud', 'network', 'security'], + uiInternalPath: '/app/home#/tutorial/gcpLogs', + description: 'Collect Google Cloud audit, firewall, and VPC flow logs.', + icons: [ + { + type: 'eui', + src: 'logoGoogleG', + }, + ], + shipper: 'beats', + eprOverlap: 'gcp', + isBeta: false, + }, + { + type: 'ui_link', + id: 'GSuite logs', + title: 'GSuite logs', + categories: ['security'], + uiInternalPath: '/app/home#/tutorial/gsuiteLogs', + description: 'Collect GSuite activity reports.', + icons: [ + { + type: 'eui', + src: 'logoGoogleG', + }, + ], + shipper: 'beats', + eprOverlap: 'gsuite', + isBeta: false, + }, + { + type: 'ui_link', + id: 'HAProxy logs', + title: 'HAProxy logs', + categories: ['network', 'web'], + uiInternalPath: '/app/home#/tutorial/haproxyLogs', + description: 'Collect HAProxy logs.', + icons: [ + { + type: 'eui', + src: 'logoHAproxy', + }, + ], + shipper: 'beats', + eprOverlap: 'haproxy', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Icinga logs', + title: 'Icinga logs', + categories: ['security'], + uiInternalPath: '/app/home#/tutorial/icingaLogs', + description: 'Collect Icinga main, debug, and startup logs.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/icinga.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'icinga', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Imperva logs', + title: 'Imperva logs', + categories: ['network', 'security'], + uiInternalPath: '/app/home#/tutorial/impervaLogs', + description: 'Collect Imperva SecureSphere logs over syslog or from a file.', + icons: [ + { + type: 'eui', + src: 'logoLogging', + }, + ], + shipper: 'beats', + eprOverlap: 'imperva', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Infoblox logs', + title: 'Infoblox logs', + categories: ['network'], + uiInternalPath: '/app/home#/tutorial/infobloxLogs', + description: 'Collect Infoblox NIOS logs over syslog or from a file.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/infoblox.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'infoblox', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Juniper Logs', + title: 'Juniper Logs', + categories: ['network', 'security'], + uiInternalPath: '/app/home#/tutorial/juniperLogs', + description: 'Collect Juniper JUNOS logs over syslog or from a file.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/juniper.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'juniper', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Kibana Logs', + title: 'Kibana Logs', + categories: ['message_queue'], + uiInternalPath: '/app/home#/tutorial/kibanaLogs', + description: 'Collect Kibana logs.', + icons: [ + { + type: 'eui', + src: 'logoKibana', + }, + ], + shipper: 'beats', + eprOverlap: 'kibana', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Microsoft Defender ATP logs', + title: 'Microsoft Defender ATP logs', + categories: ['network', 'security', 'azure'], + uiInternalPath: '/app/home#/tutorial/microsoftLogs', + description: 'Collect Microsoft Defender ATP alerts.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/microsoft.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'microsoft', + isBeta: false, + }, + { + type: 'ui_link', + id: 'MISP threat intel logs', + title: 'MISP threat intel logs', + categories: ['network', 'security', 'azure'], + uiInternalPath: '/app/home#/tutorial/mispLogs', + description: 'Collect MISP threat intelligence data with Filebeat.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/misp.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'misp', + isBeta: false, + }, + { + type: 'ui_link', + id: 'MongoDB logs', + title: 'MongoDB logs', + categories: ['datastore'], + uiInternalPath: '/app/home#/tutorial/mongodbLogs', + description: 'Collect MongoDB logs.', + icons: [ + { + type: 'eui', + src: 'logoMongodb', + }, + ], + shipper: 'beats', + eprOverlap: 'mongodb', + isBeta: false, + }, + { + type: 'ui_link', + id: 'MSSQL logs', + title: 'MSSQL logs', + categories: ['datastore'], + uiInternalPath: '/app/home#/tutorial/mssqlLogs', + description: 'Collect MSSQL logs.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/microsoft.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'mssql', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Arbor Peakflow logs', + title: 'Arbor Peakflow logs', + categories: ['security'], + uiInternalPath: '/app/home#/tutorial/netscoutLogs', + description: 'Collect Netscout Arbor Peakflow SP logs over syslog or from a file.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/netscout.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'netscout', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Office 365 logs', + title: 'Office 365 logs', + categories: ['security'], + uiInternalPath: '/app/home#/tutorial/o365Logs', + description: 'Collect Office 365 activity logs via the Office 365 API.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/o365.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'o365', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Okta logs', + title: 'Okta logs', + categories: ['security'], + uiInternalPath: '/app/home#/tutorial/oktaLogs', + description: 'Collect the Okta system log via the Okta API.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/okta.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'okta', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Palo Alto Networks PAN-OS logs', + title: 'Palo Alto Networks PAN-OS logs', + categories: ['security'], + uiInternalPath: '/app/home#/tutorial/panwLogs', + description: + 'Collect Palo Alto Networks PAN-OS threat and traffic logs over syslog or from a log file.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/paloalto.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'panw', + isBeta: false, + }, + { + type: 'ui_link', + id: 'RabbitMQ logs', + title: 'RabbitMQ logs', + categories: ['message_queue'], + uiInternalPath: '/app/home#/tutorial/rabbitmqLogs', + description: 'Collect RabbitMQ logs.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/rabbitmq.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'rabbitmq', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Radware DefensePro logs', + title: 'Radware DefensePro logs', + categories: ['security'], + uiInternalPath: '/app/home#/tutorial/radwareLogs', + description: 'Collect Radware DefensePro logs over syslog or from a file.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/radware.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'radware', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Google Santa logs', + title: 'Google Santa logs', + categories: ['security', 'os_system'], + uiInternalPath: '/app/home#/tutorial/santaLogs', + description: 'Collect Google Santa logs about process executions on MacOS.', + icons: [ + { + type: 'eui', + src: 'logoLogging', + }, + ], + shipper: 'beats', + eprOverlap: 'santa', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Sonicwall FW logs', + title: 'Sonicwall FW logs', + categories: ['network', 'security'], + uiInternalPath: '/app/home#/tutorial/sonicwallLogs', + description: 'Collect Sonicwall-FW logs over syslog or from a file.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/sonicwall.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'sonicwall', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Sophos logs', + title: 'Sophos logs', + categories: ['security'], + uiInternalPath: '/app/home#/tutorial/sophosLogs', + description: 'Collect Sophos XG SFOS logs over syslog.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/sophos.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'sophos', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Squid logs', + title: 'Squid logs', + categories: ['security'], + uiInternalPath: '/app/home#/tutorial/squidLogs', + description: 'Collect Squid logs over syslog or from a file.', + icons: [ + { + type: 'eui', + src: 'logoLogging', + }, + ], + shipper: 'beats', + eprOverlap: 'squid', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Tomcat logs', + title: 'Tomcat logs', + categories: ['web', 'security'], + uiInternalPath: '/app/home#/tutorial/tomcatLogs', + description: 'Collect Apache Tomcat logs over syslog or from a file.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/tomcat.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'tomcat', + isBeta: false, + }, + { + type: 'ui_link', + id: 'Zscaler Logs', + title: 'Zscaler Logs', + categories: ['network', 'security'], + uiInternalPath: '/app/home#/tutorial/zscalerLogs', + description: 'This is a module for receiving Zscaler NSS logs over Syslog or a file.', + icons: [ + { + type: 'svg', + src: '/dqo/plugins/home/assets/logos/zscaler.svg', + }, + ], + shipper: 'beats', + eprOverlap: 'zscaler', + isBeta: false, + }, + { + type: 'ui_link', + id: 'apm', + title: 'APM', + categories: ['web'], + uiInternalPath: '/app/home#/tutorial/apm', + description: 'Collect in-depth performance metrics and errors from inside your applications.', + icons: [ + { + type: 'eui', + src: 'apmApp', + }, + ], + shipper: 'tutorial', + isBeta: false, + eprOverlap: 'apm', + }, +]; diff --git a/src/plugins/custom_integrations/public/services/stub/index.ts b/src/plugins/custom_integrations/public/services/stub/index.ts new file mode 100644 index 0000000000000..fe7465949d565 --- /dev/null +++ b/src/plugins/custom_integrations/public/services/stub/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { + PluginServiceProviders, + PluginServiceProvider, + PluginServiceRegistry, +} from '../../../../presentation_util/public'; + +import { CustomIntegrationsServices } from '..'; +import { findServiceFactory } from './find'; +import { platformServiceFactory } from './platform'; + +export { findServiceFactory } from './find'; +export { platformServiceFactory } from './platform'; + +export const providers: PluginServiceProviders = { + find: new PluginServiceProvider(findServiceFactory), + platform: new PluginServiceProvider(platformServiceFactory), +}; + +export const registry = new PluginServiceRegistry(providers); diff --git a/src/plugins/custom_integrations/public/services/stub/platform.ts b/src/plugins/custom_integrations/public/services/stub/platform.ts new file mode 100644 index 0000000000000..60480f9905cb9 --- /dev/null +++ b/src/plugins/custom_integrations/public/services/stub/platform.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { PluginServiceFactory } from '../../../../presentation_util/public'; + +import type { CustomIntegrationsPlatformService } from '../platform'; + +/** + * A type definition for a factory to produce the `CustomIntegrationsPlatformService` with stubbed output. + * @see /src/plugins/presentation_util/public/services/create/factory.ts + */ +export type CustomIntegrationsPlatformServiceFactory = + PluginServiceFactory; + +/** + * A factory to produce the `CustomIntegrationsPlatformService` with stubbed output. + */ +export const platformServiceFactory: CustomIntegrationsPlatformServiceFactory = () => ({ + getBasePath: () => '/basePath', + getAbsolutePath: (path: string): string => `/basePath${path}`, +}); diff --git a/src/plugins/custom_integrations/public/types.ts b/src/plugins/custom_integrations/public/types.ts index 9a12af767ecbc..946115329e2b5 100755 --- a/src/plugins/custom_integrations/public/types.ts +++ b/src/plugins/custom_integrations/public/types.ts @@ -6,14 +6,19 @@ * Side Public License, v 1. */ +import type { PresentationUtilPluginStart } from '../../presentation_util/public'; + import { CustomIntegration } from '../common'; export interface CustomIntegrationsSetup { getAppendCustomIntegrations: () => Promise; getReplacementCustomIntegrations: () => Promise; } -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface CustomIntegrationsStart {} -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface AppPluginStartDependencies {} +export interface CustomIntegrationsStart { + ContextProvider: React.FC; +} + +export interface CustomIntegrationsStartDependencies { + presentationUtil: PresentationUtilPluginStart; +} diff --git a/src/plugins/custom_integrations/server/language_clients/index.ts b/src/plugins/custom_integrations/server/language_clients/index.ts new file mode 100644 index 0000000000000..da61f804b4242 --- /dev/null +++ b/src/plugins/custom_integrations/server/language_clients/index.ts @@ -0,0 +1,181 @@ +/* + * 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'; +import { CoreSetup } from 'kibana/server'; +import { CustomIntegrationRegistry } from '../custom_integration_registry'; +import { CustomIntegrationIcon, PLUGIN_ID } from '../../common'; + +interface LanguageIntegration { + id: string; + title: string; + icon?: string; + euiIconName?: string; + description: string; + docUrlTemplate: string; +} + +const ELASTIC_WEBSITE_URL = 'https://www.elastic.co'; +const ELASTICSEARCH_CLIENT_URL = `${ELASTIC_WEBSITE_URL}/guide/en/elasticsearch/client`; +export const integrations: LanguageIntegration[] = [ + { + id: 'all', + title: i18n.translate('customIntegrations.languageclients.AllTitle', { + defaultMessage: 'Elasticsearch Clients', + }), + euiIconName: 'logoElasticsearch', + description: i18n.translate('customIntegrations.languageclients.AllDescription', { + defaultMessage: + 'Start building your custom application on top of Elasticsearch with the official language clients.', + }), + docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/index.html`, + }, + { + id: 'javascript', + title: i18n.translate('customIntegrations.languageclients.JavascriptTitle', { + defaultMessage: 'Elasticsearch JavaScript Client', + }), + icon: 'nodejs.svg', + description: i18n.translate('customIntegrations.languageclients.JavascriptDescription', { + defaultMessage: + 'Start building your custom application on top of Elasticsearch with the official Node.js client.', + }), + docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/javascript-api/{branch}/introduction.html`, + }, + { + id: 'ruby', + title: i18n.translate('customIntegrations.languageclients.RubyTitle', { + defaultMessage: 'Elasticsearch Ruby Client', + }), + icon: 'ruby.svg', + description: i18n.translate('customIntegrations.languageclients.RubyDescription', { + defaultMessage: + 'Start building your custom application on top of Elasticsearch with the official Ruby client.', + }), + docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/ruby-api/{branch}/ruby_client.html`, + }, + { + id: 'go', + title: i18n.translate('customIntegrations.languageclients.GoTitle', { + defaultMessage: 'Elasticsearch Go Client', + }), + icon: 'go.svg', + description: i18n.translate('customIntegrations.languageclients.GoDescription', { + defaultMessage: + 'Start building your custom application on top of Elasticsearch with the official Go client.', + }), + docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/go-api/{branch}/overview.html`, + }, + { + id: 'dotnet', + title: i18n.translate('customIntegrations.languageclients.DotNetTitle', { + defaultMessage: 'Elasticsearch .NET Client', + }), + icon: 'dotnet.svg', + description: i18n.translate('customIntegrations.languageclients.DotNetDescription', { + defaultMessage: + 'Start building your custom application on top of Elasticsearch with the official .NET client.', + }), + docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/net-api/{branch}/index.html`, + }, + { + id: 'php', + title: i18n.translate('customIntegrations.languageclients.PhpTitle', { + defaultMessage: 'Elasticsearch PHP Client', + }), + icon: 'php.svg', + description: i18n.translate('customIntegrations.languageclients.PhpDescription', { + defaultMessage: + 'Start building your custom application on top of Elasticsearch with the official .PHP client.', + }), + docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/php-api/{branch}/index.html`, + }, + { + id: 'perl', + title: i18n.translate('customIntegrations.languageclients.PerlTitle', { + defaultMessage: 'Elasticsearch Perl Client', + }), + icon: 'perl.svg', + description: i18n.translate('customIntegrations.languageclients.PerlDescription', { + defaultMessage: + 'Start building your custom application on top of Elasticsearch with the official Perl client.', + }), + docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/perl-api/{branch}/index.html`, + }, + { + id: 'python', + title: i18n.translate('customIntegrations.languageclients.PythonTitle', { + defaultMessage: 'Elasticsearch Python Client', + }), + icon: 'python.svg', + description: i18n.translate('customIntegrations.languageclients.PythonDescription', { + defaultMessage: + 'Start building your custom application on top of Elasticsearch with the official Python client.', + }), + docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/python-api/{branch}/index.html`, + }, + { + id: 'rust', + title: i18n.translate('customIntegrations.languageclients.RustTitle', { + defaultMessage: 'Elasticsearch Rust Client', + }), + icon: 'rust.svg', + description: i18n.translate('customIntegrations.languageclients.RustDescription', { + defaultMessage: + 'Start building your custom application on top of Elasticsearch with the official Rust client.', + }), + docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/rust-api/{branch}/index.html`, + }, + { + id: 'java', + title: i18n.translate('customIntegrations.languageclients.JavaTitle', { + defaultMessage: 'Elasticsearch Java Client', + }), + icon: 'java.svg', + description: i18n.translate('customIntegrations.languageclients.JavaDescription', { + defaultMessage: + 'Start building your custom application on top of Elasticsearch with the official Java client.', + }), + docUrlTemplate: `${ELASTICSEARCH_CLIENT_URL}/java-api-client/{branch}/index.html`, + }, +]; + +export function registerLanguageClients( + core: CoreSetup, + registry: CustomIntegrationRegistry, + branch: string +) { + integrations.forEach((integration: LanguageIntegration) => { + const icons: CustomIntegrationIcon[] = []; + if (integration.euiIconName) { + icons.push({ + type: 'eui', + src: integration.euiIconName, + }); + } else if (integration.icon) { + icons.push({ + type: 'svg', + src: core.http.basePath.prepend( + `/plugins/${PLUGIN_ID}/assets/language_clients/${integration.icon}` + ), + }); + } + + registry.registerCustomIntegration({ + id: `language_client.${integration.id}`, + title: integration.title, + description: integration.description, + type: 'ui_link', + shipper: 'language_clients', + uiInternalPath: integration.docUrlTemplate.replace('{branch}', branch), + isBeta: false, + icons, + categories: ['elastic_stack', 'custom', 'language_client'], + }); + }); +} diff --git a/src/plugins/custom_integrations/server/plugin.test.ts b/src/plugins/custom_integrations/server/plugin.test.ts index 424eedf0603cd..8dee81ba6cba3 100644 --- a/src/plugins/custom_integrations/server/plugin.test.ts +++ b/src/plugins/custom_integrations/server/plugin.test.ts @@ -22,9 +22,145 @@ describe('CustomIntegrationsPlugin', () => { initContext = coreMock.createPluginInitializerContext(); }); - test('wires up tutorials provider service and returns registerTutorial and addScopedTutorialContextFactory', () => { + test('should return setup contract', () => { const setup = new CustomIntegrationsPlugin(initContext).setup(mockCoreSetup); expect(setup).toHaveProperty('registerCustomIntegration'); + expect(setup).toHaveProperty('getAppendCustomIntegrations'); + }); + + test('should register language clients', () => { + const setup = new CustomIntegrationsPlugin(initContext).setup(mockCoreSetup); + expect(setup.getAppendCustomIntegrations()).toEqual([ + { + id: 'language_client.all', + title: 'Elasticsearch Clients', + description: + 'Start building your custom application on top of Elasticsearch with the official language clients.', + type: 'ui_link', + shipper: 'language_clients', + uiInternalPath: 'https://www.elastic.co/guide/en/elasticsearch/client/index.html', + isBeta: false, + icons: [{ type: 'eui', src: 'logoElasticsearch' }], + categories: ['elastic_stack', 'custom', 'language_client'], + }, + { + id: 'language_client.javascript', + title: 'Elasticsearch JavaScript Client', + description: + 'Start building your custom application on top of Elasticsearch with the official Node.js client.', + type: 'ui_link', + shipper: 'language_clients', + uiInternalPath: + 'https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/branch/introduction.html', + isBeta: false, + icons: [{ type: 'svg' }], + categories: ['elastic_stack', 'custom', 'language_client'], + }, + { + id: 'language_client.ruby', + title: 'Elasticsearch Ruby Client', + description: + 'Start building your custom application on top of Elasticsearch with the official Ruby client.', + type: 'ui_link', + shipper: 'language_clients', + uiInternalPath: + 'https://www.elastic.co/guide/en/elasticsearch/client/ruby-api/branch/ruby_client.html', + isBeta: false, + icons: [{ type: 'svg' }], + categories: ['elastic_stack', 'custom', 'language_client'], + }, + { + id: 'language_client.go', + title: 'Elasticsearch Go Client', + description: + 'Start building your custom application on top of Elasticsearch with the official Go client.', + type: 'ui_link', + shipper: 'language_clients', + uiInternalPath: + 'https://www.elastic.co/guide/en/elasticsearch/client/go-api/branch/overview.html', + isBeta: false, + icons: [{ type: 'svg' }], + categories: ['elastic_stack', 'custom', 'language_client'], + }, + { + id: 'language_client.dotnet', + title: 'Elasticsearch .NET Client', + description: + 'Start building your custom application on top of Elasticsearch with the official .NET client.', + type: 'ui_link', + shipper: 'language_clients', + uiInternalPath: + 'https://www.elastic.co/guide/en/elasticsearch/client/net-api/branch/index.html', + isBeta: false, + icons: [{ type: 'svg' }], + categories: ['elastic_stack', 'custom', 'language_client'], + }, + { + id: 'language_client.php', + title: 'Elasticsearch PHP Client', + description: + 'Start building your custom application on top of Elasticsearch with the official .PHP client.', + type: 'ui_link', + shipper: 'language_clients', + uiInternalPath: + 'https://www.elastic.co/guide/en/elasticsearch/client/php-api/branch/index.html', + isBeta: false, + icons: [{ type: 'svg' }], + categories: ['elastic_stack', 'custom', 'language_client'], + }, + { + id: 'language_client.perl', + title: 'Elasticsearch Perl Client', + description: + 'Start building your custom application on top of Elasticsearch with the official Perl client.', + type: 'ui_link', + shipper: 'language_clients', + uiInternalPath: + 'https://www.elastic.co/guide/en/elasticsearch/client/perl-api/branch/index.html', + isBeta: false, + icons: [{ type: 'svg' }], + categories: ['elastic_stack', 'custom', 'language_client'], + }, + { + id: 'language_client.python', + title: 'Elasticsearch Python Client', + description: + 'Start building your custom application on top of Elasticsearch with the official Python client.', + type: 'ui_link', + shipper: 'language_clients', + uiInternalPath: + 'https://www.elastic.co/guide/en/elasticsearch/client/python-api/branch/index.html', + isBeta: false, + icons: [{ type: 'svg' }], + categories: ['elastic_stack', 'custom', 'language_client'], + }, + { + id: 'language_client.rust', + title: 'Elasticsearch Rust Client', + description: + 'Start building your custom application on top of Elasticsearch with the official Rust client.', + type: 'ui_link', + shipper: 'language_clients', + uiInternalPath: + 'https://www.elastic.co/guide/en/elasticsearch/client/rust-api/branch/index.html', + isBeta: false, + icons: [{ type: 'svg' }], + categories: ['elastic_stack', 'custom', 'language_client'], + }, + { + id: 'language_client.java', + title: 'Elasticsearch Java Client', + description: + 'Start building your custom application on top of Elasticsearch with the official Java client.', + type: 'ui_link', + shipper: 'language_clients', + uiInternalPath: + 'https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/branch/index.html', + isBeta: false, + icons: [{ type: 'svg' }], + categories: ['elastic_stack', 'custom', 'language_client'], + }, + ]); }); }); }); diff --git a/src/plugins/custom_integrations/server/plugin.ts b/src/plugins/custom_integrations/server/plugin.ts index 099650ee15a05..330a1288d05a2 100755 --- a/src/plugins/custom_integrations/server/plugin.ts +++ b/src/plugins/custom_integrations/server/plugin.ts @@ -12,12 +12,14 @@ import { CustomIntegrationsPluginSetup, CustomIntegrationsPluginStart } from './ import { CustomIntegration } from '../common'; import { CustomIntegrationRegistry } from './custom_integration_registry'; import { defineRoutes } from './routes/define_routes'; +import { registerLanguageClients } from './language_clients'; export class CustomIntegrationsPlugin implements Plugin { private readonly logger: Logger; private readonly customIngegrationRegistry: CustomIntegrationRegistry; + private readonly branch: string; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); @@ -25,6 +27,7 @@ export class CustomIntegrationsPlugin this.logger, initializerContext.env.mode.dev ); + this.branch = initializerContext.env.packageInfo.branch; } public setup(core: CoreSetup) { @@ -33,6 +36,8 @@ export class CustomIntegrationsPlugin const router = core.http.createRouter(); defineRoutes(router, this.customIngegrationRegistry); + registerLanguageClients(core, this.customIngegrationRegistry, this.branch); + return { registerCustomIntegration: (integration: Omit) => { this.customIngegrationRegistry.registerCustomIntegration({ @@ -40,6 +45,9 @@ export class CustomIntegrationsPlugin ...integration, }); }, + getAppendCustomIntegrations: () => { + return this.customIngegrationRegistry.getAppendCustomIntegrations(); + }, } as CustomIntegrationsPluginSetup; } diff --git a/src/plugins/custom_integrations/storybook/decorator.tsx b/src/plugins/custom_integrations/storybook/decorator.tsx new file mode 100644 index 0000000000000..c5fea9615ee47 --- /dev/null +++ b/src/plugins/custom_integrations/storybook/decorator.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { DecoratorFn } from '@storybook/react'; +import { I18nProvider } from '@kbn/i18n/react'; + +import { PluginServiceRegistry } from '../../presentation_util/public'; + +import { pluginServices } from '../public/services'; +import { CustomIntegrationsServices } from '../public/services'; +import { providers } from '../public/services/storybook'; +import { EuiThemeProvider } from '../../kibana_react/common/eui_styled_components'; + +/** + * Returns a Storybook Decorator that provides both the `I18nProvider` and access to `PluginServices` + * for components rendered in Storybook. + */ +export const getCustomIntegrationsContextDecorator = + (): DecoratorFn => + (story, { globals }) => { + const ContextProvider = getCustomIntegrationsContextProvider(); + const darkMode = globals.euiTheme === 'v8.dark' || globals.euiTheme === 'v7.dark'; + + return ( + + + {story()} + + + ); + }; + +/** + * Prepares `PluginServices` for use in Storybook and returns a React `Context.Provider` element + * so components that access `PluginServices` can be rendered. + */ +export const getCustomIntegrationsContextProvider = () => { + const registry = new PluginServiceRegistry(providers); + pluginServices.setRegistry(registry.start({})); + return pluginServices.getContextProvider(); +}; diff --git a/src/plugins/custom_integrations/storybook/index.ts b/src/plugins/custom_integrations/storybook/index.ts new file mode 100644 index 0000000000000..a9e34e1aeeb7e --- /dev/null +++ b/src/plugins/custom_integrations/storybook/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { + getCustomIntegrationsContextDecorator as getStorybookContextDecorator, + getCustomIntegrationsContextProvider as getStorybookContextProvider, +} from '../storybook/decorator'; diff --git a/src/plugins/custom_integrations/storybook/main.ts b/src/plugins/custom_integrations/storybook/main.ts new file mode 100644 index 0000000000000..1261fe5a06f69 --- /dev/null +++ b/src/plugins/custom_integrations/storybook/main.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { defaultConfig } from '@kbn/storybook'; + +module.exports = defaultConfig; diff --git a/src/plugins/custom_integrations/storybook/manager.ts b/src/plugins/custom_integrations/storybook/manager.ts new file mode 100644 index 0000000000000..99c01efdddfdc --- /dev/null +++ b/src/plugins/custom_integrations/storybook/manager.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 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 { addons } from '@storybook/addons'; +import { create } from '@storybook/theming'; +import { PANEL_ID } from '@storybook/addon-actions'; + +addons.setConfig({ + theme: create({ + base: 'light', + brandTitle: 'Kibana Custom Integrations Storybook', + brandUrl: 'https://github.com/elastic/kibana/tree/master/src/plugins/custom_integrations', + }), + showPanel: true.valueOf, + selectedPanel: PANEL_ID, +}); diff --git a/src/plugins/custom_integrations/storybook/preview.tsx b/src/plugins/custom_integrations/storybook/preview.tsx new file mode 100644 index 0000000000000..c27390261c920 --- /dev/null +++ b/src/plugins/custom_integrations/storybook/preview.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { Title, Subtitle, Description, Primary, Stories } from '@storybook/addon-docs/blocks'; + +import { getCustomIntegrationsContextDecorator } from './decorator'; + +export const decorators = [getCustomIntegrationsContextDecorator()]; + +export const parameters = { + docs: { + page: () => ( + <> + + <Subtitle /> + <Description /> + <Primary /> + <Stories /> + </> + ), + }, +}; diff --git a/src/plugins/custom_integrations/tsconfig.json b/src/plugins/custom_integrations/tsconfig.json index 2ce7bf9c8112c..ccb75c358611b 100644 --- a/src/plugins/custom_integrations/tsconfig.json +++ b/src/plugins/custom_integrations/tsconfig.json @@ -6,8 +6,15 @@ "declaration": true, "declarationMap": true }, - "include": ["common/**/*", "public/**/*", "server/**/*"], + "include": [ + "../../../typings/**/*", + "common/**/*", + "public/**/*", + "server/**/*", + "storybook/**/*" + ], "references": [ - { "path": "../../core/tsconfig.json" } + { "path": "../../core/tsconfig.json" }, + { "path": "../presentation_util/tsconfig.json" } ] } diff --git a/src/plugins/embeddable/public/lib/containers/container.ts b/src/plugins/embeddable/public/lib/containers/container.ts index a1d4b5b68d20d..06133fb2160c0 100644 --- a/src/plugins/embeddable/public/lib/containers/container.ts +++ b/src/plugins/embeddable/public/lib/containers/container.ts @@ -46,6 +46,7 @@ export abstract class Container< parent?: Container ) { super(input, output, parent); + this.getFactory = getFactory; // Currently required for using in storybook due to https://github.com/storybookjs/storybook/issues/13834 this.subscription = this.getInput$() // At each update event, get both the previous and current state .pipe(startWith(input), pairwise()) diff --git a/src/plugins/es_ui_shared/public/request/use_request.test.ts b/src/plugins/es_ui_shared/public/request/use_request.test.ts index 68edde1336728..a6c22073dbc90 100644 --- a/src/plugins/es_ui_shared/public/request/use_request.test.ts +++ b/src/plugins/es_ui_shared/public/request/use_request.test.ts @@ -308,6 +308,24 @@ describe('useRequest hook', () => { expect(getSendRequestSpy().callCount).toBe(2); }); + it(`changing pollIntervalMs to undefined cancels the poll`, async () => { + const { setupErrorRequest, setErrorResponse, completeRequest, getSendRequestSpy } = helpers; + // Send initial request. + setupErrorRequest({ pollIntervalMs: REQUEST_TIME }); + + // Setting the poll to undefined will cancel subsequent requests. + setErrorResponse({ pollIntervalMs: undefined }); + + // Complete initial request. + await completeRequest(); + + // If there were another scheduled poll request, this would complete it. + await completeRequest(); + + // But because we canceled the poll, we only see 1 request instead of 2. + expect(getSendRequestSpy().callCount).toBe(1); + }); + it('when the path changes after a request is scheduled, the scheduled request is sent with that path', async () => { const { setupSuccessRequest, diff --git a/src/plugins/home/server/index.ts b/src/plugins/home/server/index.ts index 9523766596fed..c75ce4e83921c 100644 --- a/src/plugins/home/server/index.ts +++ b/src/plugins/home/server/index.ts @@ -18,9 +18,6 @@ export const config: PluginConfigDescriptor<ConfigSchema> = { disableWelcomeScreen: true, }, schema: configSchema, - deprecations: ({ renameFromRoot }) => [ - renameFromRoot('kibana.disableWelcomeScreen', 'home.disableWelcomeScreen'), - ], }; export const plugin = (initContext: PluginInitializerContext) => new HomeServerPlugin(initContext); diff --git a/src/plugins/newsfeed/server/config.ts b/src/plugins/newsfeed/server/config.ts index f8924706b751c..f14f3452761e1 100644 --- a/src/plugins/newsfeed/server/config.ts +++ b/src/plugins/newsfeed/server/config.ts @@ -11,7 +11,6 @@ import { NEWSFEED_DEFAULT_SERVICE_PATH, NEWSFEED_DEFAULT_SERVICE_BASE_URL, NEWSFEED_DEV_SERVICE_BASE_URL, - NEWSFEED_FALLBACK_LANGUAGE, } from '../common/constants'; export const configSchema = schema.object({ @@ -25,7 +24,6 @@ export const configSchema = schema.object({ schema.string({ defaultValue: NEWSFEED_DEV_SERVICE_BASE_URL }) ), }), - defaultLanguage: schema.string({ defaultValue: NEWSFEED_FALLBACK_LANGUAGE }), // TODO: Deprecate since no longer used mainInterval: schema.duration({ defaultValue: '2m' }), // (2min) How often to retry failed fetches, and/or check if newsfeed items need to be refreshed from remote fetchInterval: schema.duration({ defaultValue: '1d' }), // (1day) How often to fetch remote and reset the last fetched time }); diff --git a/src/plugins/newsfeed/server/index.ts b/src/plugins/newsfeed/server/index.ts index 460d48622af69..fefb725e2804e 100644 --- a/src/plugins/newsfeed/server/index.ts +++ b/src/plugins/newsfeed/server/index.ts @@ -17,7 +17,6 @@ export const config: PluginConfigDescriptor<NewsfeedConfigType> = { mainInterval: true, fetchInterval: true, }, - deprecations: ({ unused }) => [unused('defaultLanguage')], }; export function plugin() { diff --git a/src/plugins/presentation_util/public/components/controls/__stories__/controls_service_stub.ts b/src/plugins/presentation_util/public/components/controls/__stories__/controls_service_stub.ts new file mode 100644 index 0000000000000..59e7a44a83a17 --- /dev/null +++ b/src/plugins/presentation_util/public/components/controls/__stories__/controls_service_stub.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 { InputControlFactory } from '../types'; +import { ControlsService } from '../controls_service'; +import { flightFields, getEuiSelectableOptions } from './flights'; +import { OptionsListEmbeddableFactory } from '../control_types/options_list'; + +export const getControlsServiceStub = () => { + const controlsServiceStub = new ControlsService(); + + const optionsListFactoryStub = new OptionsListEmbeddableFactory( + ({ field, search }) => + new Promise((r) => setTimeout(() => r(getEuiSelectableOptions(field, search)), 500)), + () => Promise.resolve(['demo data flights']), + () => Promise.resolve(flightFields) + ); + + // cast to unknown because the stub cannot use the embeddable start contract to transform the EmbeddableFactoryDefinition into an EmbeddableFactory + const optionsListControlFactory = optionsListFactoryStub as unknown as InputControlFactory; + optionsListControlFactory.getDefaultInput = () => ({}); + controlsServiceStub.registerInputControlType(optionsListControlFactory); + return controlsServiceStub; +}; diff --git a/src/plugins/presentation_util/public/components/input_controls/__stories__/decorators.tsx b/src/plugins/presentation_util/public/components/controls/__stories__/decorators.tsx similarity index 95% rename from src/plugins/presentation_util/public/components/input_controls/__stories__/decorators.tsx rename to src/plugins/presentation_util/public/components/controls/__stories__/decorators.tsx index 0aaa0e7a8a533..c5d3cf2c815be 100644 --- a/src/plugins/presentation_util/public/components/input_controls/__stories__/decorators.tsx +++ b/src/plugins/presentation_util/public/components/controls/__stories__/decorators.tsx @@ -23,7 +23,7 @@ const panelStyle = { const kqlBarStyle = { background: bar, padding: 16, minHeight, fontStyle: 'italic' }; -const inputBarStyle = { background: '#fff', padding: 4, minHeight }; +const inputBarStyle = { background: '#fff', padding: 4 }; const layout = (OptionStory: Story) => ( <EuiFlexGroup style={{ background }} direction="column"> diff --git a/src/plugins/presentation_util/public/components/input_controls/__stories__/flights.ts b/src/plugins/presentation_util/public/components/controls/__stories__/flights.ts similarity index 100% rename from src/plugins/presentation_util/public/components/input_controls/__stories__/flights.ts rename to src/plugins/presentation_util/public/components/controls/__stories__/flights.ts diff --git a/src/plugins/presentation_util/public/components/controls/__stories__/input_controls.stories.tsx b/src/plugins/presentation_util/public/components/controls/__stories__/input_controls.stories.tsx new file mode 100644 index 0000000000000..2a463fece18da --- /dev/null +++ b/src/plugins/presentation_util/public/components/controls/__stories__/input_controls.stories.tsx @@ -0,0 +1,53 @@ +/* + * 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, { useEffect, useMemo } from 'react'; +import uuid from 'uuid'; + +import { decorators } from './decorators'; +import { providers } from '../../../services/storybook'; +import { getControlsServiceStub } from './controls_service_stub'; +import { ControlGroupContainerFactory } from '../control_group/control_group_container_factory'; + +export default { + title: 'Controls', + description: '', + decorators, +}; + +const ControlGroupStoryComponent = () => { + const embeddableRoot: React.RefObject<HTMLDivElement> = useMemo(() => React.createRef(), []); + + providers.overlays.start({}); + const overlays = providers.overlays.getService(); + + const controlsServiceStub = getControlsServiceStub(); + + useEffect(() => { + (async () => { + const factory = new ControlGroupContainerFactory(controlsServiceStub, overlays); + const controlGroupContainerEmbeddable = await factory.create({ + inheritParentState: { + useQuery: false, + useFilters: false, + useTimerange: false, + }, + controlStyle: 'oneLine', + id: uuid.v4(), + panels: {}, + }); + if (controlGroupContainerEmbeddable && embeddableRoot.current) { + controlGroupContainerEmbeddable.render(embeddableRoot.current); + } + })(); + }, [embeddableRoot, controlsServiceStub, overlays]); + + return <div ref={embeddableRoot} />; +}; + +export const ControlGroupStory = () => <ControlGroupStoryComponent />; diff --git a/src/plugins/presentation_util/public/components/controls/control_frame/control_frame_component.tsx b/src/plugins/presentation_util/public/components/controls/control_frame/control_frame_component.tsx new file mode 100644 index 0000000000000..240beea13b0e2 --- /dev/null +++ b/src/plugins/presentation_util/public/components/controls/control_frame/control_frame_component.tsx @@ -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 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, { useEffect, useMemo, useState } from 'react'; +import classNames from 'classnames'; +import { + EuiButtonIcon, + EuiFormControlLayout, + EuiFormLabel, + EuiFormRow, + EuiToolTip, +} from '@elastic/eui'; +import { ControlGroupContainer } from '../control_group/control_group_container'; +import { useChildEmbeddable } from '../hooks/use_child_embeddable'; +import { ControlStyle } from '../types'; +import { ControlFrameStrings } from './control_frame_strings'; + +export interface ControlFrameProps { + container: ControlGroupContainer; + customPrepend?: JSX.Element; + controlStyle: ControlStyle; + enableActions?: boolean; + onRemove?: () => void; + embeddableId: string; + onEdit?: () => void; +} + +export const ControlFrame = ({ + customPrepend, + enableActions, + embeddableId, + controlStyle, + container, + onRemove, + onEdit, +}: ControlFrameProps) => { + const embeddableRoot: React.RefObject<HTMLDivElement> = useMemo(() => React.createRef(), []); + const embeddable = useChildEmbeddable({ container, embeddableId }); + + const [title, setTitle] = useState<string>(); + + const usingTwoLineLayout = controlStyle === 'twoLine'; + + useEffect(() => { + if (embeddableRoot.current && embeddable) { + embeddable.render(embeddableRoot.current); + } + const subscription = embeddable?.getInput$().subscribe((newInput) => setTitle(newInput.title)); + return () => subscription?.unsubscribe(); + }, [embeddable, embeddableRoot]); + + const floatingActions = ( + <div + className={classNames('controlFrame--floatingActions', { + 'controlFrame--floatingActions-twoLine': usingTwoLineLayout, + 'controlFrame--floatingActions-oneLine': !usingTwoLineLayout, + })} + > + <EuiToolTip content={ControlFrameStrings.floatingActions.getEditButtonTitle()}> + <EuiButtonIcon + aria-label={ControlFrameStrings.floatingActions.getEditButtonTitle()} + iconType="pencil" + onClick={onEdit} + color="text" + /> + </EuiToolTip> + <EuiToolTip content={ControlFrameStrings.floatingActions.getRemoveButtonTitle()}> + <EuiButtonIcon + aria-label={ControlFrameStrings.floatingActions.getRemoveButtonTitle()} + onClick={onRemove} + iconType="cross" + color="danger" + /> + </EuiToolTip> + </div> + ); + + const form = ( + <EuiFormControlLayout + className={'controlFrame--formControlLayout'} + fullWidth + prepend={ + <> + {customPrepend ?? null} + {usingTwoLineLayout ? undefined : ( + <EuiFormLabel className="controlFrame--formControlLayout__label" htmlFor={embeddableId}> + {title} + </EuiFormLabel> + )} + </> + } + > + <div + className={classNames('controlFrame--control', { + 'controlFrame--twoLine': controlStyle === 'twoLine', + 'controlFrame--oneLine': controlStyle === 'oneLine', + })} + id={`controlFrame--${embeddableId}`} + ref={embeddableRoot} + /> + </EuiFormControlLayout> + ); + + return ( + <> + {enableActions && floatingActions} + <EuiFormRow fullWidth label={usingTwoLineLayout ? title : undefined}> + {form} + </EuiFormRow> + </> + ); +}; diff --git a/src/plugins/presentation_util/public/components/controls/control_frame/control_frame_strings.ts b/src/plugins/presentation_util/public/components/controls/control_frame/control_frame_strings.ts new file mode 100644 index 0000000000000..5f9e89aa797cb --- /dev/null +++ b/src/plugins/presentation_util/public/components/controls/control_frame/control_frame_strings.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +export const ControlFrameStrings = { + floatingActions: { + getEditButtonTitle: () => + i18n.translate('presentationUtil.inputControls.controlGroup.floatingActions.editTitle', { + defaultMessage: 'Manage control', + }), + getRemoveButtonTitle: () => + i18n.translate('presentationUtil.inputControls.controlGroup.floatingActions.removeTitle', { + defaultMessage: 'Remove control', + }), + }, +}; diff --git a/src/plugins/presentation_util/public/components/controls/control_group/component/control_group_component.tsx b/src/plugins/presentation_util/public/components/controls/control_group/component/control_group_component.tsx new file mode 100644 index 0000000000000..d683c0749d98d --- /dev/null +++ b/src/plugins/presentation_util/public/components/controls/control_group/component/control_group_component.tsx @@ -0,0 +1,163 @@ +/* + * 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 '../control_group.scss'; + +import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import React, { useEffect, useMemo, useState } from 'react'; +import classNames from 'classnames'; +import { + arrayMove, + SortableContext, + rectSortingStrategy, + sortableKeyboardCoordinates, +} from '@dnd-kit/sortable'; +import { + closestCenter, + DndContext, + DragEndEvent, + DragOverlay, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, + LayoutMeasuringStrategy, +} from '@dnd-kit/core'; + +import { ControlGroupStrings } from '../control_group_strings'; +import { ControlGroupContainer } from '../control_group_container'; +import { ControlClone, SortableControl } from './control_group_sortable_item'; +import { OPTIONS_LIST_CONTROL } from '../../control_types/options_list/options_list_embeddable'; + +interface ControlGroupProps { + controlGroupContainer: ControlGroupContainer; +} + +export const ControlGroup = ({ controlGroupContainer }: ControlGroupProps) => { + const [controlIds, setControlIds] = useState<string[]>([]); + + // sync controlIds every time input panels change + useEffect(() => { + const subscription = controlGroupContainer.getInput$().subscribe(() => { + setControlIds((currentIds) => { + // sync control Ids with panels from container input. + const { panels } = controlGroupContainer.getInput(); + const newIds: string[] = []; + const allIds = [...currentIds, ...Object.keys(panels)]; + allIds.forEach((id) => { + const currentIndex = currentIds.indexOf(id); + if (!panels[id] && currentIndex !== -1) { + currentIds.splice(currentIndex, 1); + } + if (currentIndex === -1 && Boolean(panels[id])) { + newIds.push(id); + } + }); + return [...currentIds, ...newIds]; + }); + }); + return () => subscription.unsubscribe(); + }, [controlGroupContainer]); + + const [draggingId, setDraggingId] = useState<string | null>(null); + + const draggingIndex = useMemo( + () => (draggingId ? controlIds.indexOf(draggingId) : -1), + [controlIds, draggingId] + ); + + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) + ); + + const onDragEnd = ({ over }: DragEndEvent) => { + if (over) { + const overIndex = controlIds.indexOf(over.id); + if (draggingIndex !== overIndex) { + const newIndex = overIndex; + setControlIds((currentControlIds) => arrayMove(currentControlIds, draggingIndex, newIndex)); + } + } + setDraggingId(null); + }; + + return ( + <EuiFlexGroup wrap={false} direction="row" alignItems="center" className="superWrapper"> + <EuiFlexItem> + <DndContext + onDragStart={({ active }) => setDraggingId(active.id)} + onDragEnd={onDragEnd} + onDragCancel={() => setDraggingId(null)} + sensors={sensors} + collisionDetection={closestCenter} + layoutMeasuring={{ + strategy: LayoutMeasuringStrategy.Always, + }} + > + <SortableContext items={controlIds} strategy={rectSortingStrategy}> + <EuiFlexGroup + className={classNames('controlGroup', { 'controlGroup-isDragging': draggingId })} + alignItems="center" + gutterSize={'m'} + wrap={true} + > + {controlIds.map((controlId, index) => ( + <SortableControl + onEdit={() => controlGroupContainer.editControl(controlId)} + onRemove={() => controlGroupContainer.removeEmbeddable(controlId)} + dragInfo={{ index, draggingIndex }} + container={controlGroupContainer} + controlStyle={controlGroupContainer.getInput().controlStyle} + embeddableId={controlId} + width={controlGroupContainer.getInput().panels[controlId].width} + key={controlId} + /> + ))} + </EuiFlexGroup> + </SortableContext> + <DragOverlay> + {draggingId ? ( + <ControlClone + width={controlGroupContainer.getInput().panels[draggingId].width} + embeddableId={draggingId} + container={controlGroupContainer} + /> + ) : null} + </DragOverlay> + </DndContext> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiFlexGroup alignItems="center" direction="row" gutterSize="xs"> + <EuiFlexItem> + <EuiToolTip content={ControlGroupStrings.management.getManageButtonTitle()}> + <EuiButtonIcon + aria-label={ControlGroupStrings.management.getManageButtonTitle()} + iconType="gear" + color="text" + data-test-subj="inputControlsSortingButton" + onClick={controlGroupContainer.editControlGroup} + /> + </EuiToolTip> + </EuiFlexItem> + <EuiFlexItem> + <EuiToolTip content={ControlGroupStrings.management.getAddControlTitle()}> + <EuiButtonIcon + aria-label={ControlGroupStrings.management.getManageButtonTitle()} + iconType="plus" + color="text" + data-test-subj="inputControlsSortingButton" + onClick={() => controlGroupContainer.createNewControl(OPTIONS_LIST_CONTROL)} // use popover when there are multiple types of control + /> + </EuiToolTip> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlexItem> + </EuiFlexGroup> + ); +}; diff --git a/src/plugins/presentation_util/public/components/controls/control_group/component/control_group_sortable_item.tsx b/src/plugins/presentation_util/public/components/controls/control_group/component/control_group_sortable_item.tsx new file mode 100644 index 0000000000000..3ae171a588da4 --- /dev/null +++ b/src/plugins/presentation_util/public/components/controls/control_group/component/control_group_sortable_item.tsx @@ -0,0 +1,151 @@ +/* + * 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 { EuiFlexItem, EuiFormLabel, EuiIcon, EuiFlexGroup } from '@elastic/eui'; +import React, { forwardRef, HTMLAttributes } from 'react'; +import { useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import classNames from 'classnames'; + +import { ControlWidth } from '../../types'; +import { ControlGroupContainer } from '../control_group_container'; +import { useChildEmbeddable } from '../../hooks/use_child_embeddable'; +import { ControlFrame, ControlFrameProps } from '../../control_frame/control_frame_component'; + +interface DragInfo { + isOver?: boolean; + isDragging?: boolean; + draggingIndex?: number; + index?: number; +} + +export type SortableControlProps = ControlFrameProps & { + dragInfo: DragInfo; + width: ControlWidth; +}; + +/** + * A sortable wrapper around the generic control frame. + */ +export const SortableControl = (frameProps: SortableControlProps) => { + const { embeddableId } = frameProps; + const { over, listeners, isSorting, transform, transition, attributes, isDragging, setNodeRef } = + useSortable({ + id: embeddableId, + animateLayoutChanges: () => true, + }); + + frameProps.dragInfo = { ...frameProps.dragInfo, isOver: over?.id === embeddableId, isDragging }; + + return ( + <SortableControlInner + key={embeddableId} + ref={setNodeRef} + {...frameProps} + {...attributes} + {...listeners} + style={{ + transition: transition ?? undefined, + transform: isSorting ? undefined : CSS.Translate.toString(transform), + }} + /> + ); +}; + +const SortableControlInner = forwardRef< + HTMLButtonElement, + SortableControlProps & { style: HTMLAttributes<HTMLButtonElement>['style'] } +>( + ( + { + embeddableId, + controlStyle, + container, + dragInfo, + onRemove, + onEdit, + style, + width, + ...dragHandleProps + }, + dragHandleRef + ) => { + const { isOver, isDragging, draggingIndex, index } = dragInfo; + + const dragHandle = ( + <button ref={dragHandleRef} {...dragHandleProps} className="controlFrame--dragHandle"> + <EuiIcon type="grabHorizontal" /> + </button> + ); + + return ( + <EuiFlexItem + grow={width === 'auto'} + className={classNames('controlFrame--wrapper', { + 'controlFrame--wrapper-isDragging': isDragging, + 'controlFrame--wrapper-small': width === 'small', + 'controlFrame--wrapper-medium': width === 'medium', + 'controlFrame--wrapper-large': width === 'large', + 'controlFrame--wrapper-insertBefore': isOver && (index ?? -1) < (draggingIndex ?? -1), + 'controlFrame--wrapper-insertAfter': isOver && (index ?? -1) > (draggingIndex ?? -1), + })} + style={style} + > + <ControlFrame + enableActions={draggingIndex === -1} + controlStyle={controlStyle} + embeddableId={embeddableId} + customPrepend={dragHandle} + container={container} + onRemove={onRemove} + onEdit={onEdit} + /> + </EuiFlexItem> + ); + } +); + +/** + * A simplified clone version of the control which is dragged. This version only shows + * the title, because individual controls can be any size, and dragging a wide item + * can be quite cumbersome. + */ +export const ControlClone = ({ + embeddableId, + container, + width, +}: { + embeddableId: string; + container: ControlGroupContainer; + width: ControlWidth; +}) => { + const embeddable = useChildEmbeddable({ embeddableId, container }); + const layout = container.getInput().controlStyle; + return ( + <EuiFlexItem + className={classNames('controlFrame--cloneWrapper', { + 'controlFrame--cloneWrapper-small': width === 'small', + 'controlFrame--cloneWrapper-medium': width === 'medium', + 'controlFrame--cloneWrapper-large': width === 'large', + 'controlFrame--cloneWrapper-twoLine': layout === 'twoLine', + })} + > + {layout === 'twoLine' ? ( + <EuiFormLabel>{embeddable?.getInput().title}</EuiFormLabel> + ) : undefined} + <EuiFlexGroup gutterSize="none" className={'controlFrame--draggable'}> + <EuiFlexItem grow={false}> + <EuiIcon type="grabHorizontal" className="controlFrame--dragHandle" /> + </EuiFlexItem> + {container.getInput().controlStyle === 'oneLine' ? ( + <EuiFlexItem>{embeddable?.getInput().title}</EuiFlexItem> + ) : undefined} + </EuiFlexGroup> + </EuiFlexItem> + ); +}; diff --git a/src/plugins/presentation_util/public/components/controls/control_group/control_group.scss b/src/plugins/presentation_util/public/components/controls/control_group/control_group.scss new file mode 100644 index 0000000000000..f49efa7aab043 --- /dev/null +++ b/src/plugins/presentation_util/public/components/controls/control_group/control_group.scss @@ -0,0 +1,184 @@ +$smallControl: $euiSize * 14; +$mediumControl: $euiSize * 25; +$largeControl: $euiSize * 50; +$controlMinWidth: $euiSize * 14; + +.controlGroup { + margin-left: $euiSizeXS; + overflow-x: clip; // sometimes when using auto width, removing a control can cause a horizontal scrollbar to appear. + min-height: $euiSize * 4; + padding: $euiSize 0; +} + +.controlFrame--cloneWrapper { + width: max-content; + + .euiFormLabel { + padding-bottom: $euiSizeXS; + } + + &-small { + width: $smallControl; + } + + &-medium { + width: $mediumControl; + } + + &-large { + width: $largeControl; + } + + &-twoLine { + margin-top: -$euiSize * 1.25; + } + + .euiFormLabel, div { + cursor: grabbing !important; // prevents cursor flickering while dragging the clone + } + + .controlFrame--draggable { + cursor: grabbing; + height: $euiButtonHeight; + align-items: center; + border-radius: $euiBorderRadius; + @include euiFontSizeS; + font-weight: $euiFontWeightSemiBold; + @include euiFormControlDefaultShadow; + background-color: $euiFormInputGroupLabelBackground; + min-width: $controlMinWidth; + } + + .controlFrame--formControlLayout, .controlFrame--draggable { + &-clone { + box-shadow: 0 0 0 1px $euiShadowColor, + 0 1px 6px 0 $euiShadowColor; + cursor: grabbing !important; + } + + .controlFrame--dragHandle { + cursor: grabbing; + } + } +} + +.controlFrame--wrapper { + flex-basis: auto; + position: relative; + display: block; + + .controlFrame--formControlLayout { + width: 100%; + min-width: $controlMinWidth; + transition:background-color .1s, color .1s; + + &__label { + @include euiTextTruncate; + max-width: 50%; + } + + &:not(.controlFrame--formControlLayout-clone) { + .controlFrame--dragHandle { + cursor: grab; + } + } + + .controlFrame--control { + height: 100%; + transition: opacity .1s; + + &.controlFrame--twoLine { + width: 100%; + } + } + } + + &-small { + width: $smallControl; + } + + &-medium { + width: $mediumControl; + } + + &-large { + width: $largeControl; + } + + &-insertBefore, + &-insertAfter { + .controlFrame--formControlLayout:after { + content: ''; + position: absolute; + background-color: transparentize($euiColorPrimary, .5); + border-radius: $euiBorderRadius; + top: 0; + bottom: 0; + width: 2px; + } + } + + &-insertBefore { + .controlFrame--formControlLayout:after { + left: -$euiSizeS; + } + } + + &-insertAfter { + .controlFrame--formControlLayout:after { + right: -$euiSizeS; + } + } + + .controlFrame--floatingActions { + visibility: hidden; + opacity: 0; + + // slower transition on hover leave in case the user accidentally stops hover + transition: visibility .3s, opacity .3s; + + z-index: 1; + position: absolute; + + &-oneLine { + right:$euiSizeXS; + top: -$euiSizeL; + padding: $euiSizeXS; + border-radius: $euiBorderRadius; + background-color: $euiColorEmptyShade; + box-shadow: 0 0 0 1pt $euiColorLightShade; + } + + &-twoLine { + right:$euiSizeXS; + top: -$euiSizeXS; + } + } + + &:hover { + .controlFrame--floatingActions { + transition:visibility .1s, opacity .1s; + visibility: visible; + opacity: 1; + } + } + + &-isDragging { + .euiFormRow__labelWrapper { + opacity: 0; + } + .controlFrame--formControlLayout { + background-color: $euiColorEmptyShade !important; + color: transparent !important; + box-shadow: none; + + .euiFormLabel { + opacity: 0; + } + + .controlFrame--control { + opacity: 0; + } + } + } +} \ No newline at end of file diff --git a/src/plugins/presentation_util/public/components/controls/control_group/control_group_constants.ts b/src/plugins/presentation_util/public/components/controls/control_group/control_group_constants.ts new file mode 100644 index 0000000000000..3c22b1ffbcd23 --- /dev/null +++ b/src/plugins/presentation_util/public/components/controls/control_group/control_group_constants.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { ControlWidth } from '../types'; +import { ControlGroupStrings } from './control_group_strings'; + +export const CONTROL_GROUP_TYPE = 'control_group'; + +export const DEFAULT_CONTROL_WIDTH: ControlWidth = 'auto'; + +export const CONTROL_WIDTH_OPTIONS = [ + { + id: `auto`, + label: ControlGroupStrings.management.controlWidth.getAutoWidthTitle(), + }, + { + id: `small`, + label: ControlGroupStrings.management.controlWidth.getSmallWidthTitle(), + }, + { + id: `medium`, + label: ControlGroupStrings.management.controlWidth.getMediumWidthTitle(), + }, + { + id: `large`, + label: ControlGroupStrings.management.controlWidth.getLargeWidthTitle(), + }, +]; + +export const CONTROL_LAYOUT_OPTIONS = [ + { + id: `oneLine`, + label: ControlGroupStrings.management.controlStyle.getSingleLineTitle(), + }, + { + id: `twoLine`, + label: ControlGroupStrings.management.controlStyle.getTwoLineTitle(), + }, +]; diff --git a/src/plugins/presentation_util/public/components/controls/control_group/control_group_container.tsx b/src/plugins/presentation_util/public/components/controls/control_group/control_group_container.tsx new file mode 100644 index 0000000000000..03249889dfdea --- /dev/null +++ b/src/plugins/presentation_util/public/components/controls/control_group/control_group_container.tsx @@ -0,0 +1,224 @@ +/* + * 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 ReactDOM from 'react-dom'; +import { cloneDeep } from 'lodash'; + +import { + Container, + EmbeddableFactory, + EmbeddableFactoryNotFoundError, +} from '../../../../../embeddable/public'; +import { + InputControlEmbeddable, + InputControlInput, + InputControlOutput, + IEditableControlFactory, + ControlWidth, +} from '../types'; +import { ControlsService } from '../controls_service'; +import { ControlGroupInput, ControlPanelState } from './types'; +import { ManageControlComponent } from './editor/manage_control'; +import { toMountPoint } from '../../../../../kibana_react/public'; +import { ControlGroup } from './component/control_group_component'; +import { PresentationOverlaysService } from '../../../services/overlays'; +import { CONTROL_GROUP_TYPE, DEFAULT_CONTROL_WIDTH } from './control_group_constants'; +import { ManageControlGroup } from './editor/manage_control_group_component'; +import { OverlayRef } from '../../../../../../core/public'; +import { ControlGroupStrings } from './control_group_strings'; + +export class ControlGroupContainer extends Container<InputControlInput, ControlGroupInput> { + public readonly type = CONTROL_GROUP_TYPE; + + private nextControlWidth: ControlWidth = DEFAULT_CONTROL_WIDTH; + + constructor( + initialInput: ControlGroupInput, + private readonly controlsService: ControlsService, + private readonly overlays: PresentationOverlaysService, + parent?: Container + ) { + super(initialInput, { embeddableLoaded: {} }, controlsService.getControlFactory, parent); + this.overlays = overlays; + this.controlsService = controlsService; + } + + protected createNewPanelState<TEmbeddableInput extends InputControlInput = InputControlInput>( + factory: EmbeddableFactory<InputControlInput, InputControlOutput, InputControlEmbeddable>, + partial: Partial<TEmbeddableInput> = {} + ): ControlPanelState<TEmbeddableInput> { + const panelState = super.createNewPanelState(factory, partial); + return { + order: 1, + width: this.nextControlWidth, + ...panelState, + } as ControlPanelState<TEmbeddableInput>; + } + + protected getInheritedInput(id: string): InputControlInput { + const { filters, query, timeRange, inheritParentState } = this.getInput(); + return { + filters: inheritParentState.useFilters ? filters : undefined, + query: inheritParentState.useQuery ? query : undefined, + timeRange: inheritParentState.useTimerange ? timeRange : undefined, + id, + }; + } + + public createNewControl = async (type: string) => { + const factory = this.controlsService.getControlFactory(type); + if (!factory) throw new EmbeddableFactoryNotFoundError(type); + + const initialInputPromise = new Promise<Omit<InputControlInput, 'id'>>((resolve, reject) => { + let inputToReturn: Partial<InputControlInput> = {}; + + const onCancel = (ref: OverlayRef) => { + this.overlays + .openConfirm(ControlGroupStrings.management.discardNewControl.getSubtitle(), { + confirmButtonText: ControlGroupStrings.management.discardNewControl.getConfirm(), + cancelButtonText: ControlGroupStrings.management.discardNewControl.getCancel(), + title: ControlGroupStrings.management.discardNewControl.getTitle(), + buttonColor: 'danger', + }) + .then((confirmed) => { + if (confirmed) { + reject(); + ref.close(); + } + }); + }; + + const flyoutInstance = this.overlays.openFlyout( + toMountPoint( + <ManageControlComponent + width={this.nextControlWidth} + updateTitle={(newTitle) => (inputToReturn.title = newTitle)} + updateWidth={(newWidth) => (this.nextControlWidth = newWidth)} + controlEditorComponent={(factory as IEditableControlFactory).getControlEditor?.({ + onChange: (partialInput) => { + inputToReturn = { ...inputToReturn, ...partialInput }; + }, + })} + onSave={() => { + resolve(inputToReturn); + flyoutInstance.close(); + }} + onCancel={() => onCancel(flyoutInstance)} + /> + ), + { + onClose: (flyout) => onCancel(flyout), + } + ); + }); + initialInputPromise.then( + async (explicitInput) => { + await this.addNewEmbeddable(type, explicitInput); + }, + () => {} // swallow promise rejection because it can be part of normal flow + ); + }; + + public editControl = async (embeddableId: string) => { + const panel = this.getInput().panels[embeddableId]; + const factory = this.getFactory(panel.type); + const embeddable = await this.untilEmbeddableLoaded(embeddableId); + + if (!factory) throw new EmbeddableFactoryNotFoundError(panel.type); + + const initialExplicitInput = cloneDeep(panel.explicitInput); + const initialWidth = panel.width; + + const onCancel = (ref: OverlayRef) => { + this.overlays + .openConfirm(ControlGroupStrings.management.discardChanges.getSubtitle(), { + confirmButtonText: ControlGroupStrings.management.discardChanges.getConfirm(), + cancelButtonText: ControlGroupStrings.management.discardChanges.getCancel(), + title: ControlGroupStrings.management.discardChanges.getTitle(), + buttonColor: 'danger', + }) + .then((confirmed) => { + if (confirmed) { + embeddable.updateInput(initialExplicitInput); + this.updateInput({ + panels: { + ...this.getInput().panels, + [embeddableId]: { ...this.getInput().panels[embeddableId], width: initialWidth }, + }, + }); + ref.close(); + } + }); + }; + + const flyoutInstance = this.overlays.openFlyout( + toMountPoint( + <ManageControlComponent + width={panel.width} + title={embeddable.getTitle()} + removeControl={() => this.removeEmbeddable(embeddableId)} + updateTitle={(newTitle) => embeddable.updateInput({ title: newTitle })} + controlEditorComponent={(factory as IEditableControlFactory).getControlEditor?.({ + onChange: (partialInput) => embeddable.updateInput(partialInput), + initialInput: embeddable.getInput(), + })} + onCancel={() => onCancel(flyoutInstance)} + onSave={() => flyoutInstance.close()} + updateWidth={(newWidth) => + this.updateInput({ + panels: { + ...this.getInput().panels, + [embeddableId]: { ...this.getInput().panels[embeddableId], width: newWidth }, + }, + }) + } + /> + ), + { + onClose: (flyout) => onCancel(flyout), + } + ); + }; + + public editControlGroup = () => { + const flyoutInstance = this.overlays.openFlyout( + toMountPoint( + <ManageControlGroup + controlStyle={this.getInput().controlStyle} + setControlStyle={(newStyle) => this.updateInput({ controlStyle: newStyle })} + deleteAllEmbeddables={() => { + this.overlays + .openConfirm(ControlGroupStrings.management.deleteAllControls.getSubtitle(), { + confirmButtonText: ControlGroupStrings.management.deleteAllControls.getConfirm(), + cancelButtonText: ControlGroupStrings.management.deleteAllControls.getCancel(), + title: ControlGroupStrings.management.deleteAllControls.getTitle(), + buttonColor: 'danger', + }) + .then((confirmed) => { + if (confirmed) { + Object.keys(this.getInput().panels).forEach((id) => this.removeEmbeddable(id)); + flyoutInstance.close(); + } + }); + }} + setAllPanelWidths={(newWidth) => { + const newPanels = cloneDeep(this.getInput().panels); + Object.values(newPanels).forEach((panel) => (panel.width = newWidth)); + this.updateInput({ panels: { ...newPanels, ...newPanels } }); + }} + panels={this.getInput().panels} + /> + ) + ); + }; + + public render(dom: HTMLElement) { + ReactDOM.render(<ControlGroup controlGroupContainer={this} />, dom); + } +} diff --git a/src/plugins/presentation_util/public/components/controls/control_group/control_group_container_factory.ts b/src/plugins/presentation_util/public/components/controls/control_group/control_group_container_factory.ts new file mode 100644 index 0000000000000..97ef48e6b240c --- /dev/null +++ b/src/plugins/presentation_util/public/components/controls/control_group/control_group_container_factory.ts @@ -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 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. + */ + +/* + * 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 { + Container, + ContainerOutput, + EmbeddableFactory, + EmbeddableFactoryDefinition, + ErrorEmbeddable, +} from '../../../../../embeddable/public'; +import { ControlGroupInput } from './types'; +import { ControlsService } from '../controls_service'; +import { ControlGroupStrings } from './control_group_strings'; +import { CONTROL_GROUP_TYPE } from './control_group_constants'; +import { ControlGroupContainer } from './control_group_container'; +import { PresentationOverlaysService } from '../../../services/overlays'; + +export type DashboardContainerFactory = EmbeddableFactory< + ControlGroupInput, + ContainerOutput, + ControlGroupContainer +>; +export class ControlGroupContainerFactory + implements EmbeddableFactoryDefinition<ControlGroupInput, ContainerOutput, ControlGroupContainer> +{ + public readonly isContainerType = true; + public readonly type = CONTROL_GROUP_TYPE; + public readonly controlsService: ControlsService; + private readonly overlays: PresentationOverlaysService; + + constructor(controlsService: ControlsService, overlays: PresentationOverlaysService) { + this.overlays = overlays; + this.controlsService = controlsService; + } + + public isEditable = async () => false; + + public readonly getDisplayName = () => { + return ControlGroupStrings.getEmbeddableTitle(); + }; + + public getDefaultInput(): Partial<ControlGroupInput> { + return { + panels: {}, + inheritParentState: { + useFilters: true, + useQuery: true, + useTimerange: true, + }, + }; + } + + public create = async ( + initialInput: ControlGroupInput, + parent?: Container + ): Promise<ControlGroupContainer | ErrorEmbeddable> => { + return new ControlGroupContainer(initialInput, this.controlsService, this.overlays, parent); + }; +} diff --git a/src/plugins/presentation_util/public/components/controls/control_group/control_group_strings.ts b/src/plugins/presentation_util/public/components/controls/control_group/control_group_strings.ts new file mode 100644 index 0000000000000..78e50d8651931 --- /dev/null +++ b/src/plugins/presentation_util/public/components/controls/control_group/control_group_strings.ts @@ -0,0 +1,176 @@ +/* + * 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'; + +export const ControlGroupStrings = { + getEmbeddableTitle: () => + i18n.translate('presentationUtil.inputControls.controlGroup.title', { + defaultMessage: 'Control group', + }), + manageControl: { + getFlyoutTitle: () => + i18n.translate('presentationUtil.inputControls.controlGroup.manageControl.flyoutTitle', { + defaultMessage: 'Manage control', + }), + getTitleInputTitle: () => + i18n.translate('presentationUtil.inputControls.controlGroup.manageControl.titleInputTitle', { + defaultMessage: 'Title', + }), + getWidthInputTitle: () => + i18n.translate('presentationUtil.inputControls.controlGroup.manageControl.widthInputTitle', { + defaultMessage: 'Control width', + }), + getSaveChangesTitle: () => + i18n.translate('presentationUtil.inputControls.controlGroup.manageControl.saveChangesTitle', { + defaultMessage: 'Save and close', + }), + getCancelTitle: () => + i18n.translate('presentationUtil.inputControls.controlGroup.manageControl.cancelTitle', { + defaultMessage: 'Cancel', + }), + }, + management: { + getAddControlTitle: () => + i18n.translate('presentationUtil.inputControls.controlGroup.management.addControl', { + defaultMessage: 'Add control', + }), + getManageButtonTitle: () => + i18n.translate('presentationUtil.inputControls.controlGroup.management.buttonTitle', { + defaultMessage: 'Manage controls', + }), + getFlyoutTitle: () => + i18n.translate('presentationUtil.inputControls.controlGroup.management.flyoutTitle', { + defaultMessage: 'Manage controls', + }), + getDesignTitle: () => + i18n.translate('presentationUtil.inputControls.controlGroup.management.designTitle', { + defaultMessage: 'Design', + }), + getWidthTitle: () => + i18n.translate('presentationUtil.inputControls.controlGroup.management.widthTitle', { + defaultMessage: 'Width', + }), + getLayoutTitle: () => + i18n.translate('presentationUtil.inputControls.controlGroup.management.layoutTitle', { + defaultMessage: 'Layout', + }), + getDeleteButtonTitle: () => + i18n.translate('presentationUtil.inputControls.controlGroup.management.delete', { + defaultMessage: 'Delete control', + }), + getDeleteAllButtonTitle: () => + i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteAll', { + defaultMessage: 'Delete all', + }), + controlWidth: { + getChangeAllControlWidthsTitle: () => + i18n.translate( + 'presentationUtil.inputControls.controlGroup.management.layout.changeAllControlWidths', + { + defaultMessage: 'Set width for all controls', + } + ), + getWidthSwitchLegend: () => + i18n.translate( + 'presentationUtil.inputControls.controlGroup.management.layout.controlWidthLegend', + { + defaultMessage: 'Change individual control width', + } + ), + getAutoWidthTitle: () => + i18n.translate('presentationUtil.inputControls.controlGroup.management.layout.auto', { + defaultMessage: 'Auto', + }), + getSmallWidthTitle: () => + i18n.translate('presentationUtil.inputControls.controlGroup.management.layout.small', { + defaultMessage: 'Small', + }), + getMediumWidthTitle: () => + i18n.translate('presentationUtil.inputControls.controlGroup.management.layout.medium', { + defaultMessage: 'Medium', + }), + getLargeWidthTitle: () => + i18n.translate('presentationUtil.inputControls.controlGroup.management.layout.large', { + defaultMessage: 'Large', + }), + }, + controlStyle: { + getDesignSwitchLegend: () => + i18n.translate( + 'presentationUtil.inputControls.controlGroup.management.layout.designSwitchLegend', + { + defaultMessage: 'Switch control designs', + } + ), + getSingleLineTitle: () => + i18n.translate('presentationUtil.inputControls.controlGroup.management.layout.singleLine', { + defaultMessage: 'Single line layout', + }), + getTwoLineTitle: () => + i18n.translate('presentationUtil.inputControls.controlGroup.management.layout.twoLine', { + defaultMessage: 'Two line layout', + }), + }, + deleteAllControls: { + getTitle: () => + i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteAll.title', { + defaultMessage: 'Delete all?', + }), + getSubtitle: () => + i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteAll.sub', { + defaultMessage: 'Controls are not recoverable once removed.', + }), + getConfirm: () => + i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteAll.confirm', { + defaultMessage: 'Delete', + }), + getCancel: () => + i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteAll.cancel', { + defaultMessage: 'Cancel', + }), + }, + discardChanges: { + getTitle: () => + i18n.translate('presentationUtil.inputControls.controlGroup.management.discard.title', { + defaultMessage: 'Discard?', + }), + getSubtitle: () => + i18n.translate('presentationUtil.inputControls.controlGroup.management.discard.sub', { + defaultMessage: + 'Discard changes to this control? Controls are not recoverable once removed.', + }), + getConfirm: () => + i18n.translate('presentationUtil.inputControls.controlGroup.management.discard.confirm', { + defaultMessage: 'Discard', + }), + getCancel: () => + i18n.translate('presentationUtil.inputControls.controlGroup.management.discard.cancel', { + defaultMessage: 'Cancel', + }), + }, + discardNewControl: { + getTitle: () => + i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteNew.title', { + defaultMessage: 'Discard?', + }), + getSubtitle: () => + i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteNew.sub', { + defaultMessage: 'Discard new control? Controls are not recoverable once removed.', + }), + getConfirm: () => + i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteNew.confirm', { + defaultMessage: 'Discard', + }), + getCancel: () => + i18n.translate('presentationUtil.inputControls.controlGroup.management.deleteNew.cancel', { + defaultMessage: 'Cancel', + }), + }, + }, +}; diff --git a/src/plugins/presentation_util/public/components/controls/control_group/editor/manage_control.tsx b/src/plugins/presentation_util/public/components/controls/control_group/editor/manage_control.tsx new file mode 100644 index 0000000000000..6d80a6e0b31f6 --- /dev/null +++ b/src/plugins/presentation_util/public/components/controls/control_group/editor/manage_control.tsx @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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. + */ + +/* + * 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, { useEffect, useState } from 'react'; +import { + EuiFlyoutHeader, + EuiButtonGroup, + EuiFlyoutBody, + EuiFlexGroup, + EuiFlexItem, + EuiTitle, + EuiFieldText, + EuiFlyoutFooter, + EuiButton, + EuiFormRow, + EuiForm, + EuiButtonEmpty, + EuiSpacer, +} from '@elastic/eui'; + +import { ControlGroupStrings } from '../control_group_strings'; +import { ControlEditorComponent, ControlWidth } from '../../types'; +import { CONTROL_WIDTH_OPTIONS } from '../control_group_constants'; + +interface ManageControlProps { + title?: string; + onSave: () => void; + width: ControlWidth; + onCancel: () => void; + removeControl?: () => void; + controlEditorComponent?: ControlEditorComponent; + updateTitle: (title: string) => void; + updateWidth: (newWidth: ControlWidth) => void; +} + +export const ManageControlComponent = ({ + controlEditorComponent, + removeControl, + updateTitle, + updateWidth, + onCancel, + onSave, + title, + width, +}: ManageControlProps) => { + const [currentTitle, setCurrentTitle] = useState(title); + const [currentWidth, setCurrentWidth] = useState(width); + + const [controlEditorValid, setControlEditorValid] = useState(false); + const [editorValid, setEditorValid] = useState(false); + + useEffect(() => setEditorValid(Boolean(currentTitle)), [currentTitle]); + + return ( + <> + <EuiFlyoutHeader hasBorder> + <EuiTitle size="m"> + <h2>{ControlGroupStrings.manageControl.getFlyoutTitle()}</h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiForm> + <EuiFormRow label={ControlGroupStrings.manageControl.getTitleInputTitle()}> + <EuiFieldText + placeholder="Placeholder text" + value={currentTitle} + onChange={(e) => { + updateTitle(e.target.value); + setCurrentTitle(e.target.value); + }} + aria-label="Use aria labels when no actual label is in use" + /> + </EuiFormRow> + <EuiFormRow label={ControlGroupStrings.manageControl.getWidthInputTitle()}> + <EuiButtonGroup + color="primary" + legend={ControlGroupStrings.management.controlWidth.getWidthSwitchLegend()} + options={CONTROL_WIDTH_OPTIONS} + idSelected={currentWidth} + onChange={(newWidth: string) => { + setCurrentWidth(newWidth as ControlWidth); + updateWidth(newWidth as ControlWidth); + }} + /> + </EuiFormRow> + + <EuiSpacer size="l" /> + {controlEditorComponent && + controlEditorComponent({ setValidState: setControlEditorValid })} + <EuiSpacer size="l" /> + {removeControl && ( + <EuiButtonEmpty + aria-label={`delete-${title}`} + iconType="trash" + flush="left" + color="danger" + onClick={() => { + onCancel(); + removeControl(); + }} + > + {ControlGroupStrings.management.getDeleteButtonTitle()} + </EuiButtonEmpty> + )} + </EuiForm> + </EuiFlyoutBody> + <EuiFlyoutFooter> + <EuiFlexGroup justifyContent="spaceBetween"> + <EuiFlexItem grow={false}> + <EuiButtonEmpty + aria-label={`delete-${title}`} + iconType="cross" + onClick={() => { + onCancel(); + }} + > + {ControlGroupStrings.manageControl.getCancelTitle()} + </EuiButtonEmpty> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <EuiButton + aria-label={`delete-${title}`} + iconType="check" + color="primary" + disabled={!editorValid || !controlEditorValid} + onClick={() => { + onSave(); + }} + > + {ControlGroupStrings.manageControl.getSaveChangesTitle()} + </EuiButton> + </EuiFlexItem> + </EuiFlexGroup> + </EuiFlyoutFooter> + </> + ); +}; diff --git a/src/plugins/presentation_util/public/components/controls/control_group/editor/manage_control_group_component.tsx b/src/plugins/presentation_util/public/components/controls/control_group/editor/manage_control_group_component.tsx new file mode 100644 index 0000000000000..e766b16ade13a --- /dev/null +++ b/src/plugins/presentation_util/public/components/controls/control_group/editor/manage_control_group_component.tsx @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 useMount from 'react-use/lib/useMount'; +import React, { useState } from 'react'; +import { + EuiFlyoutHeader, + EuiButtonEmpty, + EuiButtonGroup, + EuiFlyoutBody, + EuiFormRow, + EuiSpacer, + EuiSwitch, + EuiTitle, +} from '@elastic/eui'; + +import { ControlsPanels } from '../types'; +import { ControlStyle, ControlWidth } from '../../types'; +import { ControlGroupStrings } from '../control_group_strings'; +import { CONTROL_LAYOUT_OPTIONS, CONTROL_WIDTH_OPTIONS } from '../control_group_constants'; + +interface ManageControlGroupProps { + panels: ControlsPanels; + controlStyle: ControlStyle; + deleteAllEmbeddables: () => void; + setControlStyle: (style: ControlStyle) => void; + setAllPanelWidths: (newWidth: ControlWidth) => void; +} + +export const ManageControlGroup = ({ + panels, + controlStyle, + setControlStyle, + setAllPanelWidths, + deleteAllEmbeddables, +}: ManageControlGroupProps) => { + const [currentControlStyle, setCurrentControlStyle] = useState<ControlStyle>(controlStyle); + const [selectedWidth, setSelectedWidth] = useState<ControlWidth>(); + const [selectionDisplay, setSelectionDisplay] = useState(false); + + useMount(() => { + if (!panels || Object.keys(panels).length === 0) return; + const firstWidth = panels[Object.keys(panels)[0]].width; + if (Object.values(panels).every((panel) => panel.width === firstWidth)) { + setSelectedWidth(firstWidth); + } + }); + + return ( + <> + <EuiFlyoutHeader hasBorder> + <EuiTitle size="m"> + <h2>{ControlGroupStrings.management.getFlyoutTitle()}</h2> + </EuiTitle> + </EuiFlyoutHeader> + <EuiFlyoutBody> + <EuiFormRow label={ControlGroupStrings.management.getLayoutTitle()}> + <EuiButtonGroup + color="primary" + legend={ControlGroupStrings.management.controlStyle.getDesignSwitchLegend()} + options={CONTROL_LAYOUT_OPTIONS} + idSelected={currentControlStyle} + onChange={(newControlStyle) => { + setControlStyle(newControlStyle as ControlStyle); + setCurrentControlStyle(newControlStyle as ControlStyle); + }} + /> + </EuiFormRow> + <EuiSpacer size="m" /> + <EuiFormRow label={ControlGroupStrings.management.getWidthTitle()}> + <EuiSwitch + label={ControlGroupStrings.management.controlWidth.getChangeAllControlWidthsTitle()} + checked={selectionDisplay} + onChange={() => setSelectionDisplay(!selectionDisplay)} + /> + </EuiFormRow> + {selectionDisplay ? ( + <> + <EuiSpacer size="s" /> + <EuiButtonGroup + color="primary" + idSelected={selectedWidth ?? ''} + legend={ControlGroupStrings.management.controlWidth.getWidthSwitchLegend()} + options={CONTROL_WIDTH_OPTIONS} + onChange={(newWidth: string) => { + setAllPanelWidths(newWidth as ControlWidth); + setSelectedWidth(newWidth as ControlWidth); + }} + /> + </> + ) : undefined} + + <EuiSpacer size="xl" /> + + <EuiButtonEmpty + onClick={deleteAllEmbeddables} + aria-label={'delete-all'} + iconType="trash" + color="danger" + flush="left" + size="s" + > + {ControlGroupStrings.management.getDeleteAllButtonTitle()} + </EuiButtonEmpty> + </EuiFlyoutBody> + </> + ); +}; diff --git a/src/plugins/presentation_util/public/components/controls/control_group/types.ts b/src/plugins/presentation_util/public/components/controls/control_group/types.ts new file mode 100644 index 0000000000000..fb381610711e5 --- /dev/null +++ b/src/plugins/presentation_util/public/components/controls/control_group/types.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { PanelState, EmbeddableInput } from '../../../../../embeddable/public'; +import { ControlStyle, ControlWidth, InputControlInput } from '../types'; + +export interface ControlGroupInput + extends EmbeddableInput, + Omit<InputControlInput, 'twoLineLayout'> { + inheritParentState: { + useFilters: boolean; + useQuery: boolean; + useTimerange: boolean; + }; + controlStyle: ControlStyle; + panels: ControlsPanels; +} + +export interface ControlPanelState<TEmbeddableInput extends InputControlInput = InputControlInput> + extends PanelState<TEmbeddableInput> { + order: number; + width: ControlWidth; +} + +export interface ControlsPanels { + [panelId: string]: ControlPanelState; +} diff --git a/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/index.ts b/src/plugins/presentation_util/public/components/controls/control_types/options_list/index.ts similarity index 100% rename from src/plugins/presentation_util/public/components/input_controls/control_types/options_list/index.ts rename to src/plugins/presentation_util/public/components/controls/control_types/options_list/index.ts diff --git a/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list.scss b/src/plugins/presentation_util/public/components/controls/control_types/options_list/options_list.scss similarity index 100% rename from src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list.scss rename to src/plugins/presentation_util/public/components/controls/control_types/options_list/options_list.scss diff --git a/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_component.tsx b/src/plugins/presentation_util/public/components/controls/control_types/options_list/options_list_component.tsx similarity index 97% rename from src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_component.tsx rename to src/plugins/presentation_util/public/components/controls/control_types/options_list/options_list_component.tsx index 4aff1ff4eee96..0d12c69fdab46 100644 --- a/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_component.tsx +++ b/src/plugins/presentation_util/public/components/controls/control_types/options_list/options_list_component.tsx @@ -15,7 +15,7 @@ import { OptionsListStrings } from './options_list_strings'; import { OptionsListPopover } from './options_list_popover_component'; import './options_list.scss'; -import { useStateObservable } from '../../use_state_observable'; +import { useStateObservable } from '../../hooks/use_state_observable'; export interface OptionsListComponentState { availableOptions?: EuiSelectableOption[]; diff --git a/src/plugins/presentation_util/public/components/controls/control_types/options_list/options_list_editor.tsx b/src/plugins/presentation_util/public/components/controls/control_types/options_list/options_list_editor.tsx new file mode 100644 index 0000000000000..3e5770da22ce9 --- /dev/null +++ b/src/plugins/presentation_util/public/components/controls/control_types/options_list/options_list_editor.tsx @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { EuiFormRow, EuiSuperSelect, EuiSuperSelectOption } from '@elastic/eui'; +import React, { useEffect, useState } from 'react'; +import useMount from 'react-use/lib/useMount'; +import { ControlEditorProps, GetControlEditorComponentProps } from '../../types'; +import { + OptionsListEmbeddableInput, + OptionsListFieldFetcher, + OptionsListIndexPatternFetcher, +} from './options_list_embeddable'; +import { OptionsListStrings } from './options_list_strings'; + +interface OptionsListEditorProps extends ControlEditorProps { + onChange: GetControlEditorComponentProps<OptionsListEmbeddableInput>['onChange']; + fetchIndexPatterns: OptionsListIndexPatternFetcher; + initialInput?: Partial<OptionsListEmbeddableInput>; + fetchFields: OptionsListFieldFetcher; +} + +interface OptionsListEditorState { + availableIndexPatterns: Array<EuiSuperSelectOption<string>>; + indexPattern?: string; + availableFields: Array<EuiSuperSelectOption<string>>; + field?: string; +} + +export const OptionsListEditor = ({ + onChange, + fetchFields, + initialInput, + setValidState, + fetchIndexPatterns, +}: OptionsListEditorProps) => { + const [state, setState] = useState<OptionsListEditorState>({ + indexPattern: initialInput?.indexPattern, + field: initialInput?.field, + availableIndexPatterns: [], + availableFields: [], + }); + + const applySelection = ({ field, indexPattern }: { field?: string; indexPattern?: string }) => { + const newState = { ...(field ? { field } : {}), ...(indexPattern ? { indexPattern } : {}) }; + /** + * apply state and run onChange concurrently. State is copied here rather than by subscribing to embeddable + * input so that the same editor component can cover the 'create' use case. + */ + + setState((currentState) => { + return { ...currentState, ...newState }; + }); + onChange(newState); + }; + + useMount(() => { + (async () => { + const indexPatterns = (await fetchIndexPatterns()).map((indexPattern) => ({ + value: indexPattern, + inputDisplay: indexPattern, + })); + setState((currentState) => ({ ...currentState, availableIndexPatterns: indexPatterns })); + })(); + }); + + useEffect(() => { + (async () => { + let availableFields: Array<EuiSuperSelectOption<string>> = []; + if (state.indexPattern) { + availableFields = (await fetchFields(state.indexPattern)).map((field) => ({ + value: field, + inputDisplay: field, + })); + } + setState((currentState) => ({ ...currentState, availableFields })); + })(); + }, [state.indexPattern, fetchFields]); + + useEffect( + () => setValidState(Boolean(state.field) && Boolean(state.indexPattern)), + [state.field, setValidState, state.indexPattern] + ); + + return ( + <> + <EuiFormRow label={OptionsListStrings.editor.getIndexPatternTitle()}> + <EuiSuperSelect + options={state.availableIndexPatterns} + onChange={(indexPattern) => applySelection({ indexPattern })} + valueOfSelected={state.indexPattern} + /> + </EuiFormRow> + <EuiFormRow label={OptionsListStrings.editor.getFieldTitle()}> + <EuiSuperSelect + disabled={!state.indexPattern} + options={state.availableFields} + onChange={(field) => applySelection({ field })} + valueOfSelected={state.field} + /> + </EuiFormRow> + </> + ); +}; diff --git a/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_embeddable.tsx b/src/plugins/presentation_util/public/components/controls/control_types/options_list/options_list_embeddable.tsx similarity index 91% rename from src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_embeddable.tsx rename to src/plugins/presentation_util/public/components/controls/control_types/options_list/options_list_embeddable.tsx index bdd3660606b7e..93a7b3e353bdf 100644 --- a/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_embeddable.tsx +++ b/src/plugins/presentation_util/public/components/controls/control_types/options_list/options_list_embeddable.tsx @@ -15,9 +15,9 @@ import { tap, debounceTime, map, distinctUntilChanged } from 'rxjs/operators'; import { esFilters } from '../../../../../../data/public'; import { OptionsListStrings } from './options_list_strings'; +import { Embeddable, IContainer } from '../../../../../../embeddable/public'; +import { InputControlInput, InputControlOutput } from '../../types'; import { OptionsListComponent, OptionsListComponentState } from './options_list_component'; -import { Embeddable } from '../../../../../../embeddable/public'; -import { InputControlInput, InputControlOutput } from '../../embeddable/types'; const toggleAvailableOptions = ( indices: number[], @@ -50,6 +50,9 @@ interface OptionsListDataFetchProps { timeRange?: InputControlInput['timeRange']; } +export type OptionsListIndexPatternFetcher = () => Promise<string[]>; // TODO: use the proper types here. +export type OptionsListFieldFetcher = (indexPattern: string) => Promise<string[]>; // TODO: use the proper types here. + export type OptionsListDataFetcher = ( props: OptionsListDataFetchProps ) => Promise<EuiSelectableOption[]>; @@ -58,7 +61,7 @@ export const OPTIONS_LIST_CONTROL = 'optionsListControl'; export interface OptionsListEmbeddableInput extends InputControlInput { field: string; indexPattern: string; - multiSelect: boolean; + singleSelect?: boolean; defaultSelections?: string[]; } export class OptionsListEmbeddable extends Embeddable< @@ -66,14 +69,11 @@ export class OptionsListEmbeddable extends Embeddable< InputControlOutput > { public readonly type = OPTIONS_LIST_CONTROL; - private node?: HTMLElement; - private fetchData: OptionsListDataFetcher; // internal state for this input control. private selectedOptions: Set<string>; private typeaheadSubject: Subject<string> = new Subject<string>(); - private searchString: string = ''; private componentState: OptionsListComponentState; private componentStateSubject$ = new Subject<OptionsListComponentState>(); @@ -88,9 +88,10 @@ export class OptionsListEmbeddable extends Embeddable< constructor( input: OptionsListEmbeddableInput, output: InputControlOutput, - fetchData: OptionsListDataFetcher + private fetchData: OptionsListDataFetcher, + parent?: IContainer ) { - super(input, output); + super(input, output, parent); this.fetchData = fetchData; // populate default selections from input @@ -99,7 +100,7 @@ export class OptionsListEmbeddable extends Embeddable< // fetch available options when input changes or when search string has changed const typeaheadPipe = this.typeaheadSubject.pipe( - tap((newSearchString) => (this.searchString = newSearchString)), + tap((newSearchString) => this.updateComponentState({ searchString: newSearchString })), debounceTime(100) ); const inputPipe = this.getInput$().pipe( @@ -136,7 +137,7 @@ export class OptionsListEmbeddable extends Embeddable< const { indexPattern, timeRange, filters, field, query } = this.getInput(); let newOptions = await this.fetchData({ - search: this.searchString, + search: this.componentState.searchString, indexPattern, timeRange, filters, diff --git a/src/plugins/presentation_util/public/components/controls/control_types/options_list/options_list_embeddable_factory.tsx b/src/plugins/presentation_util/public/components/controls/control_types/options_list/options_list_embeddable_factory.tsx new file mode 100644 index 0000000000000..01c31a0bcbc51 --- /dev/null +++ b/src/plugins/presentation_util/public/components/controls/control_types/options_list/options_list_embeddable_factory.tsx @@ -0,0 +1,63 @@ +/* + * 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 { EmbeddableFactoryDefinition, IContainer } from '../../../../../../embeddable/public'; +import { + ControlEditorProps, + GetControlEditorComponentProps, + IEditableControlFactory, +} from '../../types'; +import { OptionsListEditor } from './options_list_editor'; +import { + OptionsListDataFetcher, + OptionsListEmbeddable, + OptionsListEmbeddableInput, + OptionsListFieldFetcher, + OptionsListIndexPatternFetcher, + OPTIONS_LIST_CONTROL, +} from './options_list_embeddable'; + +export class OptionsListEmbeddableFactory + implements EmbeddableFactoryDefinition, IEditableControlFactory +{ + public type = OPTIONS_LIST_CONTROL; + + constructor( + private fetchData: OptionsListDataFetcher, + private fetchIndexPatterns: OptionsListIndexPatternFetcher, + private fetchFields: OptionsListFieldFetcher + ) { + this.fetchIndexPatterns = fetchIndexPatterns; + this.fetchFields = fetchFields; + this.fetchData = fetchData; + } + + public create(initialInput: OptionsListEmbeddableInput, parent?: IContainer) { + return Promise.resolve(new OptionsListEmbeddable(initialInput, {}, this.fetchData, parent)); + } + + public getControlEditor = ({ + onChange, + initialInput, + }: GetControlEditorComponentProps<OptionsListEmbeddableInput>) => { + return ({ setValidState }: ControlEditorProps) => ( + <OptionsListEditor + fetchIndexPatterns={this.fetchIndexPatterns} + fetchFields={this.fetchFields} + setValidState={setValidState} + initialInput={initialInput} + onChange={onChange} + /> + ); + }; + + public isEditable = () => Promise.resolve(false); + + public getDisplayName = () => 'Options List Control'; +} diff --git a/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_popover_component.tsx b/src/plugins/presentation_util/public/components/controls/control_types/options_list/options_list_popover_component.tsx similarity index 100% rename from src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_popover_component.tsx rename to src/plugins/presentation_util/public/components/controls/control_types/options_list/options_list_popover_component.tsx diff --git a/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_strings.ts b/src/plugins/presentation_util/public/components/controls/control_types/options_list/options_list_strings.ts similarity index 76% rename from src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_strings.ts rename to src/plugins/presentation_util/public/components/controls/control_types/options_list/options_list_strings.ts index 2211ae14cb9bd..c07881020c9c2 100644 --- a/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_strings.ts +++ b/src/plugins/presentation_util/public/components/controls/control_types/options_list/options_list_strings.ts @@ -19,6 +19,16 @@ export const OptionsListStrings = { defaultMessage: 'Select...', }), }, + editor: { + getIndexPatternTitle: () => + i18n.translate('presentationUtil.inputControls.optionsList.editor.indexPatternTitle', { + defaultMessage: 'Index pattern', + }), + getFieldTitle: () => + i18n.translate('presentationUtil.inputControls.optionsList.editor.fieldTitle', { + defaultMessage: 'Field', + }), + }, popover: { getLoadingMessage: () => i18n.translate('presentationUtil.inputControls.optionsList.popover.loading', { diff --git a/src/plugins/presentation_util/public/components/controls/controls_service.ts b/src/plugins/presentation_util/public/components/controls/controls_service.ts new file mode 100644 index 0000000000000..4e01f3cf9ab6a --- /dev/null +++ b/src/plugins/presentation_util/public/components/controls/controls_service.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { EmbeddableFactory } from '../../../../embeddable/public'; +import { + ControlTypeRegistry, + InputControlEmbeddable, + InputControlFactory, + InputControlInput, + InputControlOutput, +} from './types'; + +export class ControlsService { + private controlsFactoriesMap: ControlTypeRegistry = {}; + + public registerInputControlType = (factory: InputControlFactory) => { + this.controlsFactoriesMap[factory.type] = factory; + }; + + public getControlFactory = < + I extends InputControlInput = InputControlInput, + O extends InputControlOutput = InputControlOutput, + E extends InputControlEmbeddable<I, O> = InputControlEmbeddable<I, O> + >( + type: string + ) => { + return this.controlsFactoriesMap[type] as EmbeddableFactory<I, O, E>; + }; + + public getInputControlTypes = () => Object.keys(this.controlsFactoriesMap); +} diff --git a/src/plugins/presentation_util/public/components/controls/hooks/use_child_embeddable.ts b/src/plugins/presentation_util/public/components/controls/hooks/use_child_embeddable.ts new file mode 100644 index 0000000000000..82b9aa528bf35 --- /dev/null +++ b/src/plugins/presentation_util/public/components/controls/hooks/use_child_embeddable.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { useEffect, useState } from 'react'; +import { InputControlEmbeddable } from '../types'; +import { IContainer } from '../../../../../embeddable/public'; + +export const useChildEmbeddable = ({ + container, + embeddableId, +}: { + container: IContainer; + embeddableId: string; +}) => { + const [embeddable, setEmbeddable] = useState<InputControlEmbeddable>(); + + useEffect(() => { + let mounted = true; + (async () => { + const newEmbeddable = await container.untilEmbeddableLoaded(embeddableId); + if (!mounted) return; + setEmbeddable(newEmbeddable); + })(); + return () => { + mounted = false; + }; + }, [container, embeddableId]); + + return embeddable; +}; diff --git a/src/plugins/presentation_util/public/components/input_controls/use_state_observable.ts b/src/plugins/presentation_util/public/components/controls/hooks/use_state_observable.ts similarity index 100% rename from src/plugins/presentation_util/public/components/input_controls/use_state_observable.ts rename to src/plugins/presentation_util/public/components/controls/hooks/use_state_observable.ts diff --git a/src/plugins/presentation_util/public/components/input_controls/index.ts b/src/plugins/presentation_util/public/components/controls/index.ts similarity index 100% rename from src/plugins/presentation_util/public/components/input_controls/index.ts rename to src/plugins/presentation_util/public/components/controls/index.ts diff --git a/src/plugins/presentation_util/public/components/controls/types.ts b/src/plugins/presentation_util/public/components/controls/types.ts new file mode 100644 index 0000000000000..c94e2957e34ea --- /dev/null +++ b/src/plugins/presentation_util/public/components/controls/types.ts @@ -0,0 +1,69 @@ +/* + * 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 { Filter } from '@kbn/es-query'; +import { Query, TimeRange } from '../../../../data/public'; +import { + EmbeddableFactory, + EmbeddableInput, + EmbeddableOutput, + IEmbeddable, +} from '../../../../embeddable/public'; + +export type ControlWidth = 'auto' | 'small' | 'medium' | 'large'; +export type ControlStyle = 'twoLine' | 'oneLine'; + +/** + * Control embeddable types + */ +export type InputControlFactory = EmbeddableFactory< + InputControlInput, + InputControlOutput, + InputControlEmbeddable +>; + +export interface ControlTypeRegistry { + [key: string]: InputControlFactory; +} + +export type InputControlInput = EmbeddableInput & { + query?: Query; + filters?: Filter[]; + timeRange?: TimeRange; + twoLineLayout?: boolean; +}; + +export type InputControlOutput = EmbeddableOutput & { + filters?: Filter[]; +}; + +export type InputControlEmbeddable< + TInputControlEmbeddableInput extends InputControlInput = InputControlInput, + TInputControlEmbeddableOutput extends InputControlOutput = InputControlOutput +> = IEmbeddable<TInputControlEmbeddableInput, TInputControlEmbeddableOutput>; + +/** + * Control embeddable editor types + */ +export interface IEditableControlFactory<T extends InputControlInput = InputControlInput> { + getControlEditor?: GetControlEditorComponent<T>; +} + +export type GetControlEditorComponent<T extends InputControlInput = InputControlInput> = ( + props: GetControlEditorComponentProps<T> +) => ControlEditorComponent; +export interface GetControlEditorComponentProps<T extends InputControlInput = InputControlInput> { + onChange: (partial: Partial<T>) => void; + initialInput?: Partial<T>; +} + +export type ControlEditorComponent = (props: ControlEditorProps) => JSX.Element; + +export interface ControlEditorProps { + setValidState: (valid: boolean) => void; +} diff --git a/src/plugins/presentation_util/public/components/input_controls/__stories__/input_controls.stories.tsx b/src/plugins/presentation_util/public/components/input_controls/__stories__/input_controls.stories.tsx deleted file mode 100644 index d1ad3af0daf44..0000000000000 --- a/src/plugins/presentation_util/public/components/input_controls/__stories__/input_controls.stories.tsx +++ /dev/null @@ -1,88 +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, { useEffect, useMemo, useState } from 'react'; - -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; - -import { decorators } from './decorators'; -import { getEuiSelectableOptions, flightFields, flightFieldLabels, FlightField } from './flights'; -import { OptionsListEmbeddableFactory, OptionsListEmbeddable } from '../control_types/options_list'; -import { ControlFrame } from '../control_frame/control_frame'; - -export default { - title: 'Input Controls', - description: '', - decorators, -}; - -interface OptionsListStorybookArgs { - fields: string[]; - twoLine: boolean; -} - -const storybookArgs = { - twoLine: false, - fields: ['OriginCityName', 'OriginWeather', 'DestCityName', 'DestWeather'], -}; - -const storybookArgTypes = { - fields: { - twoLine: { - control: { type: 'bool' }, - }, - control: { - type: 'check', - options: flightFields, - }, - }, -}; - -const OptionsListStoryComponent = ({ fields, twoLine }: OptionsListStorybookArgs) => { - const [embeddables, setEmbeddables] = useState<OptionsListEmbeddable[]>([]); - - const optionsListEmbeddableFactory = useMemo( - () => - new OptionsListEmbeddableFactory( - ({ field, search }) => - new Promise((r) => setTimeout(() => r(getEuiSelectableOptions(field, search)), 500)) - ), - [] - ); - - useEffect(() => { - const embeddableCreatePromises = fields.map((field) => { - return optionsListEmbeddableFactory.create({ - field, - id: '', - indexPattern: '', - multiSelect: true, - twoLineLayout: twoLine, - title: flightFieldLabels[field as FlightField], - }); - }); - Promise.all(embeddableCreatePromises).then((newEmbeddables) => setEmbeddables(newEmbeddables)); - }, [fields, optionsListEmbeddableFactory, twoLine]); - - return ( - <EuiFlexGroup alignItems="center" wrap={true} gutterSize={'s'}> - {embeddables.map((embeddable) => ( - <EuiFlexItem key={embeddable.getInput().field}> - <ControlFrame twoLine={twoLine} embeddable={embeddable} /> - </EuiFlexItem> - ))} - </EuiFlexGroup> - ); -}; - -export const OptionsListStory = ({ fields, twoLine }: OptionsListStorybookArgs) => ( - <OptionsListStoryComponent fields={fields} twoLine={twoLine} /> -); - -OptionsListStory.args = storybookArgs; -OptionsListStory.argTypes = storybookArgTypes; diff --git a/src/plugins/presentation_util/public/components/input_controls/control_frame/control_frame.scss b/src/plugins/presentation_util/public/components/input_controls/control_frame/control_frame.scss deleted file mode 100644 index ad054be022c32..0000000000000 --- a/src/plugins/presentation_util/public/components/input_controls/control_frame/control_frame.scss +++ /dev/null @@ -1,14 +0,0 @@ -.controlFrame--formControlLayout { - width: 100%; - min-width: $euiSize * 12.5; -} - -.controlFrame--control { - &.optionsList--filterBtnSingle { - height: 100%; - } -} - -.optionsList--filterBtnTwoLine { - width: 100%; -} \ No newline at end of file diff --git a/src/plugins/presentation_util/public/components/input_controls/control_frame/control_frame.tsx b/src/plugins/presentation_util/public/components/input_controls/control_frame/control_frame.tsx deleted file mode 100644 index 7fa8688ffb368..0000000000000 --- a/src/plugins/presentation_util/public/components/input_controls/control_frame/control_frame.tsx +++ /dev/null @@ -1,58 +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, { useMemo } from 'react'; -import useMount from 'react-use/lib/useMount'; -import classNames from 'classnames'; -import { EuiFormControlLayout, EuiFormLabel, EuiFormRow } from '@elastic/eui'; - -import { InputControlEmbeddable } from '../embeddable/types'; - -import './control_frame.scss'; - -interface ControlFrameProps { - embeddable: InputControlEmbeddable; - twoLine?: boolean; -} - -export const ControlFrame = ({ twoLine, embeddable }: ControlFrameProps) => { - const embeddableRoot: React.RefObject<HTMLDivElement> = useMemo(() => React.createRef(), []); - - useMount(() => { - if (embeddableRoot.current && embeddable) embeddable.render(embeddableRoot.current); - }); - - const form = ( - <EuiFormControlLayout - className="controlFrame--formControlLayout" - fullWidth - prepend={ - twoLine ? undefined : ( - <EuiFormLabel htmlFor={embeddable.id}>{embeddable.getInput().title}</EuiFormLabel> - ) - } - > - <div - className={classNames('controlFrame--control', { - 'optionsList--filterBtnTwoLine': twoLine, - 'optionsList--filterBtnSingle': !twoLine, - })} - id={embeddable.id} - ref={embeddableRoot} - /> - </EuiFormControlLayout> - ); - - return twoLine ? ( - <EuiFormRow fullWidth label={embeddable.getInput().title}> - {form} - </EuiFormRow> - ) : ( - form - ); -}; diff --git a/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_embeddable_factory.ts b/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_embeddable_factory.ts deleted file mode 100644 index e1850e6715e34..0000000000000 --- a/src/plugins/presentation_util/public/components/input_controls/control_types/options_list/options_list_embeddable_factory.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 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 { EmbeddableFactoryDefinition } from '../../../../../../embeddable/public'; -import { - OptionsListDataFetcher, - OptionsListEmbeddable, - OptionsListEmbeddableInput, - OPTIONS_LIST_CONTROL, -} from './options_list_embeddable'; - -export class OptionsListEmbeddableFactory implements EmbeddableFactoryDefinition { - public type = OPTIONS_LIST_CONTROL; - private fetchData: OptionsListDataFetcher; - - constructor(fetchData: OptionsListDataFetcher) { - this.fetchData = fetchData; - } - - public create(initialInput: OptionsListEmbeddableInput) { - return Promise.resolve(new OptionsListEmbeddable(initialInput, {}, this.fetchData)); - } - - public isEditable = () => Promise.resolve(false); - - public getDisplayName = () => 'Options List Control'; -} diff --git a/src/plugins/presentation_util/public/components/input_controls/embeddable/types.ts b/src/plugins/presentation_util/public/components/input_controls/embeddable/types.ts deleted file mode 100644 index 00be17932ba1f..0000000000000 --- a/src/plugins/presentation_util/public/components/input_controls/embeddable/types.ts +++ /dev/null @@ -1,23 +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 { Filter, Query, TimeRange } from '../../../../../data/public'; -import { EmbeddableInput, EmbeddableOutput, IEmbeddable } from '../../../../../embeddable/public'; - -export type InputControlInput = EmbeddableInput & { - filters?: Filter[]; - query?: Query; - timeRange?: TimeRange; - twoLineLayout?: boolean; -}; - -export type InputControlOutput = EmbeddableOutput & { - filters?: Filter[]; -}; - -export type InputControlEmbeddable = IEmbeddable<InputControlInput, InputControlOutput>; diff --git a/src/plugins/presentation_util/public/services/index.ts b/src/plugins/presentation_util/public/services/index.ts index d68779b129ca6..c622ad82bb888 100644 --- a/src/plugins/presentation_util/public/services/index.ts +++ b/src/plugins/presentation_util/public/services/index.ts @@ -12,6 +12,7 @@ import { PresentationCapabilitiesService } from './capabilities'; import { PresentationDashboardsService } from './dashboards'; import { PresentationLabsService } from './labs'; import { registry as stubRegistry } from './stub'; +import { PresentationOverlaysService } from './overlays'; export { PresentationCapabilitiesService } from './capabilities'; export { PresentationDashboardsService } from './dashboards'; @@ -19,6 +20,7 @@ export { PresentationLabsService } from './labs'; export interface PresentationUtilServices { dashboards: PresentationDashboardsService; capabilities: PresentationCapabilitiesService; + overlays: PresentationOverlaysService; labs: PresentationLabsService; } diff --git a/src/plugins/presentation_util/public/services/kibana/index.ts b/src/plugins/presentation_util/public/services/kibana/index.ts index 880f0f8b49c76..8a9a28606f24b 100644 --- a/src/plugins/presentation_util/public/services/kibana/index.ts +++ b/src/plugins/presentation_util/public/services/kibana/index.ts @@ -8,6 +8,7 @@ import { capabilitiesServiceFactory } from './capabilities'; import { dashboardsServiceFactory } from './dashboards'; +import { overlaysServiceFactory } from './overlays'; import { labsServiceFactory } from './labs'; import { PluginServiceProviders, @@ -20,6 +21,7 @@ import { PresentationUtilServices } from '..'; export { capabilitiesServiceFactory } from './capabilities'; export { dashboardsServiceFactory } from './dashboards'; +export { overlaysServiceFactory } from './overlays'; export { labsServiceFactory } from './labs'; export const providers: PluginServiceProviders< @@ -29,6 +31,7 @@ export const providers: PluginServiceProviders< capabilities: new PluginServiceProvider(capabilitiesServiceFactory), labs: new PluginServiceProvider(labsServiceFactory), dashboards: new PluginServiceProvider(dashboardsServiceFactory), + overlays: new PluginServiceProvider(overlaysServiceFactory), }; export const registry = new PluginServiceRegistry< diff --git a/src/plugins/presentation_util/public/services/kibana/overlays.ts b/src/plugins/presentation_util/public/services/kibana/overlays.ts new file mode 100644 index 0000000000000..b3a8d3a6e040a --- /dev/null +++ b/src/plugins/presentation_util/public/services/kibana/overlays.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 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 { PresentationUtilPluginStartDeps } from '../../types'; +import { KibanaPluginServiceFactory } from '../create'; +import { PresentationOverlaysService } from '../overlays'; + +export type OverlaysServiceFactory = KibanaPluginServiceFactory< + PresentationOverlaysService, + PresentationUtilPluginStartDeps +>; +export const overlaysServiceFactory: OverlaysServiceFactory = ({ coreStart }) => { + const { + overlays: { openFlyout, openConfirm }, + } = coreStart; + + return { + openFlyout, + openConfirm, + }; +}; diff --git a/src/plugins/presentation_util/public/services/overlays.ts b/src/plugins/presentation_util/public/services/overlays.ts new file mode 100644 index 0000000000000..ee90de5231896 --- /dev/null +++ b/src/plugins/presentation_util/public/services/overlays.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 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 { + MountPoint, + OverlayFlyoutOpenOptions, + OverlayModalConfirmOptions, + OverlayRef, +} from '../../../../core/public'; + +export interface PresentationOverlaysService { + openFlyout(mount: MountPoint, options?: OverlayFlyoutOpenOptions): OverlayRef; + openConfirm(message: MountPoint | string, options?: OverlayModalConfirmOptions): Promise<boolean>; +} diff --git a/src/plugins/presentation_util/public/services/storybook/index.ts b/src/plugins/presentation_util/public/services/storybook/index.ts index 40fdc40a4632e..1ce1eb72848c9 100644 --- a/src/plugins/presentation_util/public/services/storybook/index.ts +++ b/src/plugins/presentation_util/public/services/storybook/index.ts @@ -11,6 +11,7 @@ import { dashboardsServiceFactory } from '../stub/dashboards'; import { labsServiceFactory } from './labs'; import { capabilitiesServiceFactory } from './capabilities'; import { PresentationUtilServices } from '..'; +import { overlaysServiceFactory } from './overlays'; export { PluginServiceProviders, PluginServiceProvider, PluginServiceRegistry } from '../create'; export { PresentationUtilServices } from '..'; @@ -25,6 +26,7 @@ export interface StorybookParams { export const providers: PluginServiceProviders<PresentationUtilServices, StorybookParams> = { capabilities: new PluginServiceProvider(capabilitiesServiceFactory), dashboards: new PluginServiceProvider(dashboardsServiceFactory), + overlays: new PluginServiceProvider(overlaysServiceFactory), labs: new PluginServiceProvider(labsServiceFactory), }; diff --git a/src/plugins/presentation_util/public/services/storybook/overlays.tsx b/src/plugins/presentation_util/public/services/storybook/overlays.tsx new file mode 100644 index 0000000000000..50194fb636fa4 --- /dev/null +++ b/src/plugins/presentation_util/public/services/storybook/overlays.tsx @@ -0,0 +1,147 @@ +/* + * 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 { EuiConfirmModal, EuiFlyout } from '@elastic/eui'; +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { Subject } from 'rxjs'; +import { + MountPoint, + OverlayFlyoutOpenOptions, + OverlayModalConfirmOptions, + OverlayRef, +} from '../../../../../core/public'; +import { MountWrapper } from '../../../../../core/public/utils'; +import { PluginServiceFactory } from '../create'; +import { PresentationOverlaysService } from '../overlays'; + +type OverlaysServiceFactory = PluginServiceFactory<PresentationOverlaysService>; + +/** + * This code is a storybook stub version of src/core/public/overlays/overlay_service.ts + * Eventually, core services should have simple storybook representations, but until that happens + * it is necessary to recreate their functionality here. + */ +class GenericOverlayRef implements OverlayRef { + public readonly onClose: Promise<void>; + private closeSubject = new Subject<void>(); + + constructor() { + this.onClose = this.closeSubject.toPromise(); + } + + public close(): Promise<void> { + if (!this.closeSubject.closed) { + this.closeSubject.next(); + this.closeSubject.complete(); + } + return this.onClose; + } +} + +export const overlaysServiceFactory: OverlaysServiceFactory = () => { + const flyoutDomElement = document.createElement('div'); + const modalDomElement = document.createElement('div'); + let activeFlyout: OverlayRef | null; + let activeModal: OverlayRef | null; + + const cleanupModal = () => { + if (modalDomElement != null) { + unmountComponentAtNode(modalDomElement); + modalDomElement.innerHTML = ''; + } + activeModal = null; + }; + + const cleanupFlyout = () => { + if (flyoutDomElement != null) { + unmountComponentAtNode(flyoutDomElement); + flyoutDomElement.innerHTML = ''; + } + activeFlyout = null; + }; + + return { + openFlyout: (mount: MountPoint, options?: OverlayFlyoutOpenOptions) => { + if (activeFlyout) { + activeFlyout.close(); + cleanupFlyout(); + } + + const flyout = new GenericOverlayRef(); + + flyout.onClose.then(() => { + if (activeFlyout === flyout) { + cleanupFlyout(); + } + }); + + activeFlyout = flyout; + + const onCloseFlyout = () => { + if (options?.onClose) { + options?.onClose(flyout); + return; + } + flyout.close(); + }; + + render( + <EuiFlyout onClose={onCloseFlyout}> + <MountWrapper mount={mount} className="kbnOverlayMountWrapper" /> + </EuiFlyout>, + flyoutDomElement + ); + + return flyout; + }, + openConfirm: (message: MountPoint | string, options?: OverlayModalConfirmOptions) => { + if (activeModal) { + activeModal.close(); + cleanupModal(); + } + + return new Promise((resolve, reject) => { + let resolved = false; + const closeModal = (confirmed: boolean) => { + resolved = true; + modal.close(); + resolve(confirmed); + }; + + const modal = new GenericOverlayRef(); + modal.onClose.then(() => { + if (activeModal === modal) { + cleanupModal(); + } + // modal.close can be called when opening a new modal/confirm, so we need to resolve the promise in that case. + if (!resolved) { + closeModal(false); + } + }); + activeModal = modal; + + const props = { + ...options, + children: + typeof message === 'string' ? ( + message + ) : ( + <MountWrapper mount={message} className="kbnOverlayMountWrapper" /> + ), + onCancel: () => closeModal(false), + onConfirm: () => closeModal(true), + cancelButtonText: options?.cancelButtonText || '', // stub default cancel text + confirmButtonText: options?.confirmButtonText || '', // stub default confirm text + }; + + render(<EuiConfirmModal {...props} />, modalDomElement); + }); + }, + }; +}; diff --git a/src/plugins/presentation_util/public/services/stub/index.ts b/src/plugins/presentation_util/public/services/stub/index.ts index 6bf32bba00a3e..61dca47427531 100644 --- a/src/plugins/presentation_util/public/services/stub/index.ts +++ b/src/plugins/presentation_util/public/services/stub/index.ts @@ -11,6 +11,7 @@ import { dashboardsServiceFactory } from './dashboards'; import { labsServiceFactory } from './labs'; import { PluginServiceProviders, PluginServiceProvider, PluginServiceRegistry } from '../create'; import { PresentationUtilServices } from '..'; +import { overlaysServiceFactory } from './overlays'; export { dashboardsServiceFactory } from './dashboards'; export { capabilitiesServiceFactory } from './capabilities'; @@ -18,6 +19,7 @@ export { capabilitiesServiceFactory } from './capabilities'; export const providers: PluginServiceProviders<PresentationUtilServices> = { dashboards: new PluginServiceProvider(dashboardsServiceFactory), capabilities: new PluginServiceProvider(capabilitiesServiceFactory), + overlays: new PluginServiceProvider(overlaysServiceFactory), labs: new PluginServiceProvider(labsServiceFactory), }; diff --git a/src/plugins/presentation_util/public/services/stub/overlays.ts b/src/plugins/presentation_util/public/services/stub/overlays.ts new file mode 100644 index 0000000000000..ecdec96d600d8 --- /dev/null +++ b/src/plugins/presentation_util/public/services/stub/overlays.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { + MountPoint, + OverlayFlyoutOpenOptions, + OverlayModalConfirmOptions, + OverlayRef, +} from '../../../../../core/public'; +import { PluginServiceFactory } from '../create'; +import { PresentationOverlaysService } from '../overlays'; + +type OverlaysServiceFactory = PluginServiceFactory<PresentationOverlaysService>; + +class StubRef implements OverlayRef { + public readonly onClose: Promise<void> = Promise.resolve(); + + public close(): Promise<void> { + return this.onClose; + } +} + +export const overlaysServiceFactory: OverlaysServiceFactory = () => ({ + openFlyout: (mount: MountPoint, options?: OverlayFlyoutOpenOptions) => new StubRef(), + openConfirm: (message: MountPoint | string, options?: OverlayModalConfirmOptions) => + Promise.resolve(true), +}); diff --git a/src/plugins/usage_collection/server/config.ts b/src/plugins/usage_collection/server/config.ts index faf8ce7535e8a..f09dc7d431b33 100644 --- a/src/plugins/usage_collection/server/config.ts +++ b/src/plugins/usage_collection/server/config.ts @@ -29,12 +29,6 @@ export type ConfigType = TypeOf<typeof configSchema>; export const config: PluginConfigDescriptor<ConfigType> = { schema: configSchema, - deprecations: ({ renameFromRoot }) => [ - renameFromRoot('ui_metric.enabled', 'usageCollection.uiCounters.enabled'), - renameFromRoot('ui_metric.debug', 'usageCollection.uiCounters.debug'), - renameFromRoot('usageCollection.uiMetric.enabled', 'usageCollection.uiCounters.enabled'), - renameFromRoot('usageCollection.uiMetric.debug', 'usageCollection.uiCounters.debug'), - ], exposeToBrowser: { uiCounters: true, }, diff --git a/test/api_integration/apis/custom_integration/integrations.ts b/test/api_integration/apis/custom_integration/integrations.ts index 4b1344ee8e84c..4b0745574b521 100644 --- a/test/api_integration/apis/custom_integration/integrations.ts +++ b/test/api_integration/apis/custom_integration/integrations.ts @@ -21,7 +21,9 @@ export default function ({ getService }: FtrProviderContext) { .expect(200); expect(resp.body).to.be.an('array'); - expect(resp.body.length).to.be.above(2); // Should at least have registered the three sample data-sets + + // sample data + expect(resp.body.length).to.be.above(13); // at least the language clients + tutorials + sample data ['flights', 'logs', 'ecommerce'].forEach((sampleData) => { expect(resp.body.findIndex((c: { id: string }) => c.id === sampleData)).to.be.above(-1); @@ -37,7 +39,8 @@ export default function ({ getService }: FtrProviderContext) { .expect(200); expect(resp.body).to.be.an('array'); - expect(resp.body.length).to.be.above(2); // Should have at least a few beats registered + + expect(resp.body.length).to.be.above(109); // at least the beats + apm }); }); }); diff --git a/test/scripts/jenkins_storybook.sh b/test/scripts/jenkins_storybook.sh index 17ca46b0097b1..bf8b881a91ecd 100755 --- a/test/scripts/jenkins_storybook.sh +++ b/test/scripts/jenkins_storybook.sh @@ -8,6 +8,7 @@ yarn storybook --site apm yarn storybook --site canvas yarn storybook --site codeeditor yarn storybook --site ci_composite +yarn storybook --site custom_integrations yarn storybook --site url_template_editor yarn storybook --site dashboard yarn storybook --site dashboard_enhanced diff --git a/x-pack/plugins/actions/server/actions_client.test.ts b/x-pack/plugins/actions/server/actions_client.test.ts index 7549d2ecaab77..ca51b1cdfea1b 100644 --- a/x-pack/plugins/actions/server/actions_client.test.ts +++ b/x-pack/plugins/actions/server/actions_client.test.ts @@ -439,7 +439,6 @@ describe('create()', () => { test('throws error creating action with disabled actionType', async () => { const localConfigUtils = getActionsConfigurationUtilities({ - enabled: true, enabledActionTypes: ['some-not-ignored-action-type'], allowedHosts: ['*'], preconfiguredAlertHistoryEsIndex: false, diff --git a/x-pack/plugins/actions/server/actions_config.test.ts b/x-pack/plugins/actions/server/actions_config.test.ts index 51cd9e5599472..217f9593ee6d8 100644 --- a/x-pack/plugins/actions/server/actions_config.test.ts +++ b/x-pack/plugins/actions/server/actions_config.test.ts @@ -22,7 +22,6 @@ import moment from 'moment'; const mockLogger = loggingSystemMock.create().get() as jest.Mocked<Logger>; const defaultActionsConfig: ActionsConfig = { - enabled: false, allowedHosts: [], enabledActionTypes: [], preconfiguredAlertHistoryEsIndex: false, @@ -47,7 +46,6 @@ describe('ensureUriAllowed', () => { test('returns true when "any" hostnames are allowed', () => { const config: ActionsConfig = { ...defaultActionsConfig, - enabled: false, allowedHosts: [AllowedHosts.Any], enabledActionTypes: [], }; @@ -77,7 +75,6 @@ describe('ensureUriAllowed', () => { test('returns true when the hostname in the requested uri is in the allowedHosts', () => { const config: ActionsConfig = { ...defaultActionsConfig, - enabled: false, allowedHosts: ['github.com'], enabledActionTypes: [], }; @@ -91,7 +88,6 @@ describe('ensureHostnameAllowed', () => { test('returns true when "any" hostnames are allowed', () => { const config: ActionsConfig = { ...defaultActionsConfig, - enabled: false, allowedHosts: [AllowedHosts.Any], enabledActionTypes: [], }; @@ -112,7 +108,6 @@ describe('ensureHostnameAllowed', () => { test('returns true when the hostname in the requested uri is in the allowedHosts', () => { const config: ActionsConfig = { ...defaultActionsConfig, - enabled: false, allowedHosts: ['github.com'], enabledActionTypes: [], }; @@ -126,7 +121,6 @@ describe('isUriAllowed', () => { test('returns true when "any" hostnames are allowed', () => { const config: ActionsConfig = { ...defaultActionsConfig, - enabled: false, allowedHosts: [AllowedHosts.Any], enabledActionTypes: [], }; @@ -152,7 +146,6 @@ describe('isUriAllowed', () => { test('returns true when the hostname in the requested uri is in the allowedHosts', () => { const config: ActionsConfig = { ...defaultActionsConfig, - enabled: false, allowedHosts: ['github.com'], enabledActionTypes: [], }; @@ -166,7 +159,6 @@ describe('isHostnameAllowed', () => { test('returns true when "any" hostnames are allowed', () => { const config: ActionsConfig = { ...defaultActionsConfig, - enabled: false, allowedHosts: [AllowedHosts.Any], enabledActionTypes: [], }; @@ -181,7 +173,6 @@ describe('isHostnameAllowed', () => { test('returns true when the hostname in the requested uri is in the allowedHosts', () => { const config: ActionsConfig = { ...defaultActionsConfig, - enabled: false, allowedHosts: ['github.com'], enabledActionTypes: [], }; @@ -193,7 +184,6 @@ describe('isActionTypeEnabled', () => { test('returns true when "any" actionTypes are allowed', () => { const config: ActionsConfig = { ...defaultActionsConfig, - enabled: false, allowedHosts: [], enabledActionTypes: ['ignore', EnabledActionTypes.Any], }; @@ -203,7 +193,6 @@ describe('isActionTypeEnabled', () => { test('returns false when no actionType is allowed', () => { const config: ActionsConfig = { ...defaultActionsConfig, - enabled: false, allowedHosts: [], enabledActionTypes: [], }; @@ -213,7 +202,6 @@ describe('isActionTypeEnabled', () => { test('returns false when the actionType is not in the enabled list', () => { const config: ActionsConfig = { ...defaultActionsConfig, - enabled: false, allowedHosts: [], enabledActionTypes: ['foo'], }; @@ -223,7 +211,6 @@ describe('isActionTypeEnabled', () => { test('returns true when the actionType is in the enabled list', () => { const config: ActionsConfig = { ...defaultActionsConfig, - enabled: false, allowedHosts: [], enabledActionTypes: ['ignore', 'foo'], }; @@ -235,7 +222,6 @@ describe('ensureActionTypeEnabled', () => { test('does not throw when any actionType is allowed', () => { const config: ActionsConfig = { ...defaultActionsConfig, - enabled: false, allowedHosts: [], enabledActionTypes: ['ignore', EnabledActionTypes.Any], }; @@ -254,7 +240,6 @@ describe('ensureActionTypeEnabled', () => { test('throws when actionType is not enabled', () => { const config: ActionsConfig = { ...defaultActionsConfig, - enabled: false, allowedHosts: [], enabledActionTypes: ['ignore'], }; @@ -268,7 +253,6 @@ describe('ensureActionTypeEnabled', () => { test('does not throw when actionType is enabled', () => { const config: ActionsConfig = { ...defaultActionsConfig, - enabled: false, allowedHosts: [], enabledActionTypes: ['ignore', 'foo'], }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils_connection.test.ts b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils_connection.test.ts index 4ed9485e923a7..149ac79522f73 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils_connection.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/lib/axios_utils_connection.test.ts @@ -247,7 +247,6 @@ async function createServer(useHttps: boolean = false): Promise<CreateServerResu } const BaseActionsConfig: ActionsConfig = { - enabled: true, allowedHosts: ['*'], enabledActionTypes: ['*'], preconfiguredAlertHistoryEsIndex: false, diff --git a/x-pack/plugins/actions/server/config.test.ts b/x-pack/plugins/actions/server/config.test.ts index d99b9349e977b..7f3ce5bee6e02 100644 --- a/x-pack/plugins/actions/server/config.test.ts +++ b/x-pack/plugins/actions/server/config.test.ts @@ -29,7 +29,6 @@ describe('config validation', () => { "idleInterval": "PT1H", "pageSize": 100, }, - "enabled": true, "enabledActionTypes": Array [ "*", ], @@ -70,7 +69,6 @@ describe('config validation', () => { "idleInterval": "PT1H", "pageSize": 100, }, - "enabled": true, "enabledActionTypes": Array [ "*", ], @@ -196,7 +194,6 @@ describe('config validation', () => { "idleInterval": "PT1H", "pageSize": 100, }, - "enabled": true, "enabledActionTypes": Array [ "*", ], diff --git a/x-pack/plugins/actions/server/config.ts b/x-pack/plugins/actions/server/config.ts index 54fd0d72bccee..cf05ee9a24eec 100644 --- a/x-pack/plugins/actions/server/config.ts +++ b/x-pack/plugins/actions/server/config.ts @@ -57,7 +57,6 @@ const customHostSettingsSchema = schema.object({ export type CustomHostSettings = TypeOf<typeof customHostSettingsSchema>; export const configSchema = schema.object({ - enabled: schema.boolean({ defaultValue: true }), allowedHosts: schema.arrayOf( schema.oneOf([schema.string({ hostname: true }), schema.literal(AllowedHosts.Any)]), { diff --git a/x-pack/plugins/actions/server/index.test.ts b/x-pack/plugins/actions/server/index.test.ts deleted file mode 100644 index dbe8fca806f17..0000000000000 --- a/x-pack/plugins/actions/server/index.test.ts +++ /dev/null @@ -1,44 +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 { config } from './index'; -import { applyDeprecations, configDeprecationFactory } from '@kbn/config'; - -const CONFIG_PATH = 'xpack.actions'; -const applyStackAlertDeprecations = (settings: Record<string, unknown> = {}) => { - const deprecations = config.deprecations!(configDeprecationFactory); - const deprecationMessages: string[] = []; - const _config = { - [CONFIG_PATH]: settings, - }; - const { config: migrated } = applyDeprecations( - _config, - deprecations.map((deprecation) => ({ - deprecation, - path: CONFIG_PATH, - })), - () => - ({ message }) => - deprecationMessages.push(message) - ); - return { - messages: deprecationMessages, - migrated, - }; -}; - -describe('index', () => { - describe('deprecations', () => { - it('should deprecate .enabled flag', () => { - const { messages } = applyStackAlertDeprecations({ enabled: false }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "\\"xpack.actions.enabled\\" is deprecated. The ability to disable this plugin will be removed in 8.0.0.", - ] - `); - }); - }); -}); diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts index bf59a1a11687d..bfddd22003978 100644 --- a/x-pack/plugins/actions/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -122,16 +122,5 @@ export const config: PluginConfigDescriptor<ActionsConfig> = { }); } }, - (settings, fromPath, addDeprecation) => { - const actions = get(settings, fromPath); - if (actions?.enabled === false || actions?.enabled === true) { - addDeprecation({ - message: `"xpack.actions.enabled" is deprecated. The ability to disable this plugin will be removed in 8.0.0.`, - correctiveActions: { - manualSteps: [`Remove "xpack.actions.enabled" from your kibana configs.`], - }, - }); - } - }, ], }; diff --git a/x-pack/plugins/actions/server/lib/custom_host_settings.test.ts b/x-pack/plugins/actions/server/lib/custom_host_settings.test.ts index ec7b46e545112..48c9352566118 100644 --- a/x-pack/plugins/actions/server/lib/custom_host_settings.test.ts +++ b/x-pack/plugins/actions/server/lib/custom_host_settings.test.ts @@ -65,7 +65,6 @@ describe('custom_host_settings', () => { describe('resolveCustomHosts()', () => { const defaultActionsConfig: ActionsConfig = { - enabled: true, allowedHosts: [], enabledActionTypes: [], preconfiguredAlertHistoryEsIndex: false, diff --git a/x-pack/plugins/actions/server/plugin.test.ts b/x-pack/plugins/actions/server/plugin.test.ts index 86d2de783ebe5..08ea99df67c8e 100644 --- a/x-pack/plugins/actions/server/plugin.test.ts +++ b/x-pack/plugins/actions/server/plugin.test.ts @@ -34,7 +34,6 @@ describe('Actions Plugin', () => { beforeEach(() => { context = coreMock.createPluginInitializerContext<ActionsConfig>({ - enabled: true, enabledActionTypes: ['*'], allowedHosts: ['*'], preconfiguredAlertHistoryEsIndex: false, @@ -253,7 +252,6 @@ describe('Actions Plugin', () => { beforeEach(() => { context = coreMock.createPluginInitializerContext<ActionsConfig>({ - enabled: true, enabledActionTypes: ['*'], allowedHosts: ['*'], preconfiguredAlertHistoryEsIndex: false, diff --git a/x-pack/plugins/alerting/README.md b/x-pack/plugins/alerting/README.md index 94cdeadee97e7..58d2ca35dea7e 100644 --- a/x-pack/plugins/alerting/README.md +++ b/x-pack/plugins/alerting/README.md @@ -118,6 +118,7 @@ The following table describes the properties of the `options` object. |executor|This is where the code for the rule type lives. This is a function to be called when executing a rule on an interval basis. For full details, see the executor section below.|Function| |producer|The id of the application producing this rule type.|string| |minimumLicenseRequired|The value of a minimum license. Most of the rules are licensed as "basic".|string| +|ruleTaskTimeout|The length of time a rule can run before being cancelled due to timeout. By default, this value is "5m".|string| |useSavedObjectReferences.extractReferences|(Optional) When developing a rule type, you can choose to implement hooks for extracting saved object references from rule parameters. This hook will be invoked when a rule is created or updated. Implementing this hook is optional, but if an extract hook is implemented, an inject hook must also be implemented.|Function |useSavedObjectReferences.injectReferences|(Optional) When developing a rule type, you can choose to implement hooks for injecting saved object references into rule parameters. This hook will be invoked when a rule is retrieved (get or find). Implementing this hook is optional, but if an inject hook is implemented, an extract hook must also be implemented.|Function |isExportable|Whether the rule type is exportable from the Saved Objects Management UI.|boolean| @@ -344,6 +345,7 @@ const myRuleType: AlertType< }; }, producer: 'alerting', + ruleTaskTimeout: '10m', useSavedObjectReferences: { extractReferences: (params: Params): RuleParamsAndRefs<ExtractedParams> => { const { testSavedObjectId, ...otherParams } = params; diff --git a/x-pack/plugins/alerting/server/config.test.ts b/x-pack/plugins/alerting/server/config.test.ts index a1ae77596ccbe..63d93b9d67769 100644 --- a/x-pack/plugins/alerting/server/config.test.ts +++ b/x-pack/plugins/alerting/server/config.test.ts @@ -12,6 +12,7 @@ describe('config validation', () => { const config: Record<string, unknown> = {}; expect(configSchema.validate(config)).toMatchInlineSnapshot(` Object { + "defaultRuleTaskTimeout": "5m", "healthCheck": Object { "interval": "60m", }, diff --git a/x-pack/plugins/alerting/server/config.ts b/x-pack/plugins/alerting/server/config.ts index 47ef451ceab92..277f0c7297df9 100644 --- a/x-pack/plugins/alerting/server/config.ts +++ b/x-pack/plugins/alerting/server/config.ts @@ -20,6 +20,7 @@ export const configSchema = schema.object({ maxEphemeralActionsPerAlert: schema.number({ defaultValue: DEFAULT_MAX_EPHEMERAL_ACTIONS_PER_ALERT, }), + defaultRuleTaskTimeout: schema.string({ validate: validateDurationSchema, defaultValue: '5m' }), }); export type AlertsConfig = TypeOf<typeof configSchema>; diff --git a/x-pack/plugins/alerting/server/health/get_state.test.ts b/x-pack/plugins/alerting/server/health/get_state.test.ts index 9429dcc07d927..f4306b8250b81 100644 --- a/x-pack/plugins/alerting/server/health/get_state.test.ts +++ b/x-pack/plugins/alerting/server/health/get_state.test.ts @@ -72,6 +72,7 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { removalDelay: '1h', }, maxEphemeralActionsPerAlert: 100, + defaultRuleTaskTimeout: '20m', }), pollInterval ).subscribe(); @@ -106,6 +107,7 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { removalDelay: '1h', }, maxEphemeralActionsPerAlert: 100, + defaultRuleTaskTimeout: '20m', }), pollInterval, retryDelay @@ -151,6 +153,7 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { removalDelay: '1h', }, maxEphemeralActionsPerAlert: 100, + defaultRuleTaskTimeout: '20m', }) ).toPromise(); @@ -182,6 +185,7 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { removalDelay: '1h', }, maxEphemeralActionsPerAlert: 100, + defaultRuleTaskTimeout: '20m', }) ).toPromise(); @@ -213,6 +217,7 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { removalDelay: '1h', }, maxEphemeralActionsPerAlert: 100, + defaultRuleTaskTimeout: '20m', }) ).toPromise(); @@ -241,6 +246,7 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { removalDelay: '1h', }, maxEphemeralActionsPerAlert: 100, + defaultRuleTaskTimeout: '20m', }), retryDelay ).subscribe((status) => { @@ -272,6 +278,7 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { removalDelay: '1h', }, maxEphemeralActionsPerAlert: 100, + defaultRuleTaskTimeout: '20m', }), retryDelay ).subscribe((status) => { @@ -309,6 +316,7 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => { removalDelay: '1h', }, maxEphemeralActionsPerAlert: 100, + defaultRuleTaskTimeout: '20m', }) ).toPromise(); diff --git a/x-pack/plugins/alerting/server/index.test.ts b/x-pack/plugins/alerting/server/index.test.ts deleted file mode 100644 index b1e64935d7cd9..0000000000000 --- a/x-pack/plugins/alerting/server/index.test.ts +++ /dev/null @@ -1,44 +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 { config } from './index'; -import { applyDeprecations, configDeprecationFactory } from '@kbn/config'; - -const CONFIG_PATH = 'xpack.alerting'; -const applyStackAlertDeprecations = (settings: Record<string, unknown> = {}) => { - const deprecations = config.deprecations!(configDeprecationFactory); - const deprecationMessages: string[] = []; - const _config = { - [CONFIG_PATH]: settings, - }; - const { config: migrated } = applyDeprecations( - _config, - deprecations.map((deprecation) => ({ - deprecation, - path: CONFIG_PATH, - })), - () => - ({ message }) => - deprecationMessages.push(message) - ); - return { - messages: deprecationMessages, - migrated, - }; -}; - -describe('index', () => { - describe('deprecations', () => { - it('should deprecate .enabled flag', () => { - const { messages } = applyStackAlertDeprecations({ enabled: false }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "\\"xpack.alerting.enabled\\" is deprecated. The ability to disable this plugin will be removed in 8.0.0.", - ] - `); - }); - }); -}); diff --git a/x-pack/plugins/alerting/server/index.ts b/x-pack/plugins/alerting/server/index.ts index 3b4688173e9b5..162ee06216304 100644 --- a/x-pack/plugins/alerting/server/index.ts +++ b/x-pack/plugins/alerting/server/index.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { get } from 'lodash'; import type { PublicMethodsOf } from '@kbn/utility-types'; import { RulesClient as RulesClientClass } from './rules_client'; import { PluginConfigDescriptor, PluginInitializerContext } from '../../../../src/core/server'; @@ -58,16 +57,5 @@ export const config: PluginConfigDescriptor<AlertsConfigType> = { 'xpack.alerts.invalidateApiKeysTask.removalDelay', 'xpack.alerting.invalidateApiKeysTask.removalDelay' ), - (settings, fromPath, addDeprecation) => { - const alerting = get(settings, fromPath); - if (alerting?.enabled === false || alerting?.enabled === true) { - addDeprecation({ - message: `"xpack.alerting.enabled" is deprecated. The ability to disable this plugin will be removed in 8.0.0.`, - correctiveActions: { - manualSteps: [`Remove "xpack.alerting.enabled" from your kibana configs.`], - }, - }); - } - }, ], }; diff --git a/x-pack/plugins/alerting/server/plugin.test.ts b/x-pack/plugins/alerting/server/plugin.test.ts index 4cfa1d91758ea..6419a3ccc5c90 100644 --- a/x-pack/plugins/alerting/server/plugin.test.ts +++ b/x-pack/plugins/alerting/server/plugin.test.ts @@ -38,6 +38,7 @@ describe('Alerting Plugin', () => { removalDelay: '1h', }, maxEphemeralActionsPerAlert: 10, + defaultRuleTaskTimeout: '5m', }); plugin = new AlertingPlugin(context); @@ -71,6 +72,7 @@ describe('Alerting Plugin', () => { removalDelay: '1h', }, maxEphemeralActionsPerAlert: 10, + defaultRuleTaskTimeout: '5m', }); plugin = new AlertingPlugin(context); @@ -142,6 +144,15 @@ describe('Alerting Plugin', () => { minimumLicenseRequired: 'basic', }); }); + + it('should apply default config value for ruleTaskTimeout', async () => { + const ruleType = { + ...sampleAlertType, + minimumLicenseRequired: 'basic', + } as AlertType<never, never, never, never, never, 'default', never>; + await setup.registerType(ruleType); + expect(ruleType.ruleTaskTimeout).toBe('5m'); + }); }); }); @@ -157,6 +168,7 @@ describe('Alerting Plugin', () => { removalDelay: '1h', }, maxEphemeralActionsPerAlert: 10, + defaultRuleTaskTimeout: '5m', }); const plugin = new AlertingPlugin(context); @@ -197,6 +209,7 @@ describe('Alerting Plugin', () => { removalDelay: '1h', }, maxEphemeralActionsPerAlert: 10, + defaultRuleTaskTimeout: '5m', }); const plugin = new AlertingPlugin(context); @@ -251,6 +264,7 @@ describe('Alerting Plugin', () => { removalDelay: '1h', }, maxEphemeralActionsPerAlert: 100, + defaultRuleTaskTimeout: '5m', }); const plugin = new AlertingPlugin(context); diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index bb42beba6e237..b63fa94fbad72 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -285,6 +285,7 @@ export class AlertingPlugin { encryptedSavedObjects: plugins.encryptedSavedObjects, }); + const alertingConfig = this.config; return { registerType< Params extends AlertTypeParams = AlertTypeParams, @@ -308,7 +309,14 @@ export class AlertingPlugin { if (!(alertType.minimumLicenseRequired in LICENSE_TYPE)) { throw new Error(`"${alertType.minimumLicenseRequired}" is not a valid license type`); } - ruleTypeRegistry.register(alertType); + if (!alertType.ruleTaskTimeout) { + alertingConfig.then((config) => { + alertType.ruleTaskTimeout = config.defaultRuleTaskTimeout; + ruleTypeRegistry.register(alertType); + }); + } else { + ruleTypeRegistry.register(alertType); + } }, }; } diff --git a/x-pack/plugins/alerting/server/rule_type_registry.test.ts b/x-pack/plugins/alerting/server/rule_type_registry.test.ts index f8067a2281f65..1c44e862c261c 100644 --- a/x-pack/plugins/alerting/server/rule_type_registry.test.ts +++ b/x-pack/plugins/alerting/server/rule_type_registry.test.ts @@ -112,6 +112,32 @@ describe('register()', () => { ); }); + test('throws if AlertType ruleTaskTimeout is not a valid duration', () => { + const alertType: AlertType<never, never, never, never, never, 'default'> = { + id: 123 as unknown as string, + name: 'Test', + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + ruleTaskTimeout: '23 milisec', + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + executor: jest.fn(), + producer: 'alerts', + }; + const registry = new RuleTypeRegistry(ruleTypeRegistryParams); + + expect(() => registry.register(alertType)).toThrowError( + new Error( + `Rule type \"123\" has invalid timeout: string is not a valid duration: 23 milisec.` + ) + ); + }); + test('throws if RuleType action groups contains reserved group id', () => { const alertType: AlertType<never, never, never, never, never, 'default' | 'NotReserved'> = { id: 'test', @@ -181,6 +207,28 @@ describe('register()', () => { `); }); + test('allows an AlertType to specify a custom rule task timeout', () => { + const alertType: AlertType<never, never, never, never, never, 'default', 'backToAwesome'> = { + id: 'test', + name: 'Test', + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + ruleTaskTimeout: '13m', + executor: jest.fn(), + producer: 'alerts', + minimumLicenseRequired: 'basic', + isExportable: true, + }; + const registry = new RuleTypeRegistry(ruleTypeRegistryParams); + registry.register(alertType); + expect(registry.get('test').ruleTaskTimeout).toBe('13m'); + }); + test('throws if the custom recovery group is contained in the AlertType action groups', () => { const alertType: AlertType< never, @@ -237,6 +285,7 @@ describe('register()', () => { isExportable: true, executor: jest.fn(), producer: 'alerts', + ruleTaskTimeout: '20m', }; const registry = new RuleTypeRegistry(ruleTypeRegistryParams); registry.register(alertType); @@ -246,6 +295,7 @@ describe('register()', () => { Object { "alerting:test": Object { "createTaskRunner": [Function], + "timeout": "20m", "title": "Test", }, }, diff --git a/x-pack/plugins/alerting/server/rule_type_registry.ts b/x-pack/plugins/alerting/server/rule_type_registry.ts index 3cd21d0c64dd5..dc72b644b2c7b 100644 --- a/x-pack/plugins/alerting/server/rule_type_registry.ts +++ b/x-pack/plugins/alerting/server/rule_type_registry.ts @@ -25,6 +25,7 @@ import { getBuiltinActionGroups, RecoveredActionGroupId, ActionGroup, + validateDurationSchema, } from '../common'; import { ILicenseState } from './lib/license_state'; import { getAlertTypeFeatureUsageName } from './lib/get_alert_type_feature_usage_name'; @@ -170,6 +171,21 @@ export class RuleTypeRegistry { }) ); } + // validate ruleTypeTimeout here + if (alertType.ruleTaskTimeout) { + const invalidTimeout = validateDurationSchema(alertType.ruleTaskTimeout); + if (invalidTimeout) { + throw new Error( + i18n.translate('xpack.alerting.ruleTypeRegistry.register.invalidTimeoutAlertTypeError', { + defaultMessage: 'Rule type "{id}" has invalid timeout: {errorMessage}.', + values: { + id: alertType.id, + errorMessage: invalidTimeout, + }, + }) + ); + } + } alertType.actionVariables = normalizedActionVariables(alertType.actionVariables); const normalizedAlertType = augmentActionGroupsWithReserved< @@ -190,6 +206,7 @@ export class RuleTypeRegistry { this.taskManager.registerTaskDefinitions({ [`alerting:${alertType.id}`]: { title: alertType.name, + timeout: alertType.ruleTaskTimeout, createTaskRunner: (context: RunContext) => this.taskRunnerFactory.create< Params, diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index ba35890efd781..c73ce86acf785 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -157,8 +157,8 @@ export interface AlertType< injectReferences: (params: ExtractedParams, references: SavedObjectReference[]) => Params; }; isExportable: boolean; + ruleTaskTimeout?: string; } - export type UntypedAlertType = AlertType< AlertTypeParams, AlertTypeState, diff --git a/x-pack/plugins/apm/public/tutorial/tutorial_fleet_instructions/index.tsx b/x-pack/plugins/apm/public/tutorial/tutorial_fleet_instructions/index.tsx index c69623f92987a..175d6797bb29f 100644 --- a/x-pack/plugins/apm/public/tutorial/tutorial_fleet_instructions/index.tsx +++ b/x-pack/plugins/apm/public/tutorial/tutorial_fleet_instructions/index.tsx @@ -86,7 +86,7 @@ function TutorialFleetInstructions({ http, basePath, isDarkTheme }: Props) { 'xpack.apm.tutorial.apmServer.fleet.message', { defaultMessage: - 'The APM integration installs Elasticsearch templates and Ingest Node pipelines for APM data.', + 'The APM integration installs Elasticsearch templates and ingest pipelines for APM data.', } )} footer={ diff --git a/x-pack/plugins/banners/server/config.test.ts b/x-pack/plugins/banners/server/config.test.ts deleted file mode 100644 index f080281cf730d..0000000000000 --- a/x-pack/plugins/banners/server/config.test.ts +++ /dev/null @@ -1,38 +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 { config } from './config'; -import { getDeprecationsFor } from '../../../../src/core/server/test_utils'; - -function applyDeprecations(settings?: Record<string, any>) { - return getDeprecationsFor({ provider: config.deprecations!, settings, path: 'xpack.banners' }); -} - -describe('deprecations', () => { - it('replaces xpack.banners.placement from "header" to "top"', () => { - const { migrated } = applyDeprecations({ - placement: 'header', - }); - expect(migrated.xpack.banners.placement).toBe('top'); - }); - it('logs a warning message about xpack.banners.placement renaming', () => { - const { messages } = applyDeprecations({ - placement: 'header', - }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "The \`header\` value for xpack.banners.placement has been replaced by \`top\`", - ] - `); - }); - it('do not rename other placement values', () => { - const { migrated, messages } = applyDeprecations({ - placement: 'disabled', - }); - expect(migrated.xpack.banners.placement).toBe('disabled'); - expect(messages.length).toBe(0); - }); -}); diff --git a/x-pack/plugins/banners/server/config.ts b/x-pack/plugins/banners/server/config.ts index 37b4c57fc2ce1..cc0e18c32e310 100644 --- a/x-pack/plugins/banners/server/config.ts +++ b/x-pack/plugins/banners/server/config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { get } from 'lodash'; +// import { get } from 'lodash'; import { schema, TypeOf } from '@kbn/config-schema'; import { PluginConfigDescriptor } from 'kibana/server'; import { isHexColor } from './utils'; @@ -39,23 +39,4 @@ export type BannersConfigType = TypeOf<typeof configSchema>; export const config: PluginConfigDescriptor<BannersConfigType> = { schema: configSchema, exposeToBrowser: {}, - deprecations: () => [ - (rootConfig, fromPath, addDeprecation) => { - const pluginConfig = get(rootConfig, fromPath); - if (pluginConfig?.placement === 'header') { - addDeprecation({ - message: 'The `header` value for xpack.banners.placement has been replaced by `top`', - correctiveActions: { - manualSteps: [ - `Remove "xpack.banners.placement: header" from your kibana configs.`, - `Add "xpack.banners.placement: to" to your kibana configs instead.`, - ], - }, - }); - return { - set: [{ path: `${fromPath}.placement`, value: 'top' }], - }; - } - }, - ], }; diff --git a/x-pack/plugins/canvas/kibana.json b/x-pack/plugins/canvas/kibana.json index 772c030e11539..9c4d1b2179d82 100644 --- a/x-pack/plugins/canvas/kibana.json +++ b/x-pack/plugins/canvas/kibana.json @@ -28,7 +28,7 @@ "uiActions", "share" ], - "optionalPlugins": ["home", "reporting", "usageCollection"], + "optionalPlugins": ["home", "reporting", "spaces", "usageCollection"], "requiredBundles": [ "discover", "home", diff --git a/x-pack/plugins/canvas/public/application.tsx b/x-pack/plugins/canvas/public/application.tsx index f2fe944bfd45d..04d3958b68e36 100644 --- a/x-pack/plugins/canvas/public/application.tsx +++ b/x-pack/plugins/canvas/public/application.tsx @@ -77,7 +77,7 @@ export const renderApp = ({ <presentationUtil.ContextProvider> <I18nProvider> <Provider store={canvasStore}> - <App /> + <App history={params.history} /> </Provider> </I18nProvider> </presentationUtil.ContextProvider> diff --git a/x-pack/plugins/canvas/public/components/app/index.tsx b/x-pack/plugins/canvas/public/components/app/index.tsx index ec9dbd47fd7c7..288ecaf83ab69 100644 --- a/x-pack/plugins/canvas/public/components/app/index.tsx +++ b/x-pack/plugins/canvas/public/components/app/index.tsx @@ -10,6 +10,7 @@ import PropTypes from 'prop-types'; import { History } from 'history'; // @ts-expect-error import createHashStateHistory from 'history-extra/dist/createHashStateHistory'; +import { ScopedHistory } from 'kibana/public'; import { useNavLinkService } from '../../services'; // @ts-expect-error import { shortcutManager } from '../../lib/shortcut_manager'; @@ -29,7 +30,7 @@ class ShortcutManagerContextWrapper extends React.Component { } } -export const App: FC = () => { +export const App: FC<{ history: ScopedHistory }> = ({ history }) => { const historyRef = useRef<History>(createHashStateHistory() as History); const { updatePath } = useNavLinkService(); @@ -39,6 +40,15 @@ export const App: FC = () => { }); }); + // We are using our own history due to needing pushState functionality not yet available on standard history + // This effect will listen for changes on the scoped history and push that to our history + // This is needed for SavedObject.resolve redirects + useEffect(() => { + return history.listen((location) => { + historyRef.current.replace(location.hash.substr(1)); + }); + }, [history]); + return ( <ShortcutManagerContextWrapper> <div className="canvas canvasContainer"> diff --git a/x-pack/plugins/canvas/public/components/fullscreen/fullscreen.scss b/x-pack/plugins/canvas/public/components/fullscreen/fullscreen.scss index 15d6b13e3fbf8..a26c264938987 100644 --- a/x-pack/plugins/canvas/public/components/fullscreen/fullscreen.scss +++ b/x-pack/plugins/canvas/public/components/fullscreen/fullscreen.scss @@ -68,4 +68,9 @@ body.canvas-isFullscreen { box-shadow: none; overflow: hidden; } + + // When in fullscreen, we want to make sure to hide the "there was a conflict" resolve callout + .canvasContainer > .euiCallOut { + display: none; + } } diff --git a/x-pack/plugins/canvas/public/components/positionable/positionable.scss b/x-pack/plugins/canvas/public/components/positionable/positionable.scss index d1d927672e052..6dd0d713a0308 100644 --- a/x-pack/plugins/canvas/public/components/positionable/positionable.scss +++ b/x-pack/plugins/canvas/public/components/positionable/positionable.scss @@ -1,4 +1,3 @@ .canvasPositionable { transform-origin: center center; /* the default, only for clarity */ - transform-style: preserve-3d; } diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index bd5d884f1485c..723d1afea2860 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -19,6 +19,7 @@ import { PluginInitializerContext, } from '../../../../src/core/public'; import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; +import { SpacesPluginStart } from '../../spaces/public'; import { initLoadingIndicator } from './lib/loading_indicator'; import { getSessionStorage } from './lib/storage'; import { SESSIONSTORAGE_LASTPATH, CANVAS_APP } from '../common/lib/constants'; @@ -62,6 +63,7 @@ export interface CanvasStartDeps { charts: ChartsPluginStart; data: DataPublicPluginStart; presentationUtil: PresentationUtilPluginStart; + spaces?: SpacesPluginStart; } /** diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.test.tsx b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.test.tsx index 0fd4d3d2401f7..846af8a891434 100644 --- a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.test.tsx +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.test.tsx @@ -11,7 +11,8 @@ import { useWorkpad } from './use_workpad'; const mockDispatch = jest.fn(); const mockSelector = jest.fn(); -const mockGetWorkpad = jest.fn(); +const mockResolveWorkpad = jest.fn(); +const mockRedirectLegacyUrl = jest.fn(); const workpad = { id: 'someworkpad', @@ -33,7 +34,10 @@ jest.mock('react-redux', () => ({ jest.mock('../../../services', () => ({ useWorkpadService: () => ({ - get: mockGetWorkpad, + resolve: mockResolveWorkpad, + }), + usePlatformService: () => ({ + redirectLegacyUrl: mockRedirectLegacyUrl, }), })); @@ -51,15 +55,59 @@ describe('useWorkpad', () => { test('fires request to load workpad and dispatches results', async () => { const workpadId = 'someworkpad'; - mockGetWorkpad.mockResolvedValue(workpadResponse); + const getRedirectPath = (id: string) => id; + mockResolveWorkpad.mockResolvedValue({ + outcome: 'exactMatch', + workpad: workpadResponse, + }); - renderHook(() => useWorkpad(workpadId)); + renderHook(() => useWorkpad(workpadId, true, getRedirectPath)); - await waitFor(() => expect(mockGetWorkpad).toHaveBeenCalledWith(workpadId)); + await waitFor(() => expect(mockResolveWorkpad).toHaveBeenCalledWith(workpadId)); - expect(mockGetWorkpad).toHaveBeenCalledWith(workpadId); + expect(mockResolveWorkpad).toHaveBeenCalledWith(workpadId); expect(mockDispatch).toHaveBeenCalledWith({ type: 'setAssets', payload: assets }); expect(mockDispatch).toHaveBeenCalledWith({ type: 'setWorkpad', payload: workpad }); expect(mockDispatch).toHaveBeenCalledWith({ type: 'setZoomScale', payload: 1 }); }); + + test('sets alias id of workpad on a conflict', async () => { + const workpadId = 'someworkpad'; + const getRedirectPath = (id: string) => id; + const aliasId = 'someworkpad-alias'; + mockResolveWorkpad.mockResolvedValue({ + outcome: 'conflict', + workpad: workpadResponse, + aliasId, + }); + + renderHook(() => useWorkpad(workpadId, true, getRedirectPath)); + + await waitFor(() => expect(mockResolveWorkpad).toHaveBeenCalledWith(workpadId)); + + expect(mockResolveWorkpad).toHaveBeenCalledWith(workpadId); + expect(mockDispatch).toHaveBeenCalledWith({ type: 'setAssets', payload: assets }); + expect(mockDispatch).toHaveBeenCalledWith({ + type: 'setWorkpad', + payload: { ...workpad, aliasId }, + }); + expect(mockDispatch).toHaveBeenCalledWith({ type: 'setZoomScale', payload: 1 }); + }); + + test('redirects on alias match', async () => { + const workpadId = 'someworkpad'; + const getRedirectPath = (id: string) => id; + const aliasId = 'someworkpad-alias'; + mockResolveWorkpad.mockResolvedValue({ + outcome: 'aliasMatch', + workpad: workpadResponse, + aliasId, + }); + + renderHook(() => useWorkpad(workpadId, true, getRedirectPath)); + + await waitFor(() => expect(mockResolveWorkpad).toHaveBeenCalledWith(workpadId)); + + expect(mockRedirectLegacyUrl).toBeCalledWith(`#${aliasId}`, 'Workpad'); + }); }); diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.ts b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.ts index 983622dad264d..f8ddd769aac43 100644 --- a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.ts +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.ts @@ -6,8 +6,9 @@ */ import { useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; import { useDispatch, useSelector } from 'react-redux'; -import { useWorkpadService } from '../../../services'; +import { useWorkpadService, usePlatformService } from '../../../services'; import { getWorkpad } from '../../../state/selectors/workpad'; import { setWorkpad } from '../../../state/actions/workpad'; // @ts-expect-error @@ -16,11 +17,18 @@ import { setAssets } from '../../../state/actions/assets'; import { setZoomScale } from '../../../state/actions/transient'; import { CanvasWorkpad } from '../../../../types'; +const getWorkpadLabel = () => + i18n.translate('xpack.canvas.workpadResolve.redirectLabel', { + defaultMessage: 'Workpad', + }); + export const useWorkpad = ( workpadId: string, - loadPages: boolean = true + loadPages: boolean = true, + getRedirectPath: (workpadId: string) => string ): [CanvasWorkpad | undefined, string | Error | undefined] => { const workpadService = useWorkpadService(); + const platformService = usePlatformService(); const dispatch = useDispatch(); const storedWorkpad = useSelector(getWorkpad); const [error, setError] = useState<string | Error | undefined>(undefined); @@ -28,15 +36,28 @@ export const useWorkpad = ( useEffect(() => { (async () => { try { - const { assets, ...workpad } = await workpadService.get(workpadId); + const { + outcome, + aliasId, + workpad: { assets, ...workpad }, + } = await workpadService.resolve(workpadId); + + if (outcome === 'conflict') { + workpad.aliasId = aliasId; + } + dispatch(setAssets(assets)); dispatch(setWorkpad(workpad, { loadPages })); dispatch(setZoomScale(1)); + + if (outcome === 'aliasMatch' && platformService.redirectLegacyUrl && aliasId) { + platformService.redirectLegacyUrl(`#${getRedirectPath(aliasId)}`, getWorkpadLabel()); + } } catch (e) { - setError(e); + setError(e as Error | string); } })(); - }, [workpadId, dispatch, setError, loadPages, workpadService]); + }, [workpadId, dispatch, setError, loadPages, workpadService, getRedirectPath, platformService]); return [storedWorkpad.id === workpadId ? storedWorkpad : undefined, error]; }; diff --git a/x-pack/plugins/canvas/public/routes/workpad/index.tsx b/x-pack/plugins/canvas/public/routes/workpad/index.tsx index 4c98511baad0b..0b6153bc06afd 100644 --- a/x-pack/plugins/canvas/public/routes/workpad/index.tsx +++ b/x-pack/plugins/canvas/public/routes/workpad/index.tsx @@ -13,6 +13,7 @@ export { WorkpadRoutingContext, WorkpadRoutingContextType } from './workpad_rout export interface WorkpadRouteParams { id: string; + pageNumber?: string; } export interface WorkpadPageRouteParams extends WorkpadRouteParams { diff --git a/x-pack/plugins/canvas/public/routes/workpad/workpad_presentation_helper.tsx b/x-pack/plugins/canvas/public/routes/workpad/workpad_presentation_helper.tsx index bdf84de7a47bd..084c9d8c76b00 100644 --- a/x-pack/plugins/canvas/public/routes/workpad/workpad_presentation_helper.tsx +++ b/x-pack/plugins/canvas/public/routes/workpad/workpad_presentation_helper.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { i18n } from '@kbn/i18n'; import React, { FC, useEffect } from 'react'; import { useSelector } from 'react-redux'; import { getBaseBreadcrumb, getWorkpadBreadcrumb } from '../../lib/breadcrumbs'; @@ -16,6 +16,11 @@ import { useAutoplayHelper } from './hooks/use_autoplay_helper'; import { useRefreshHelper } from './hooks/use_refresh_helper'; import { usePlatformService } from '../../services'; +const getWorkpadLabel = () => + i18n.translate('xpack.canvas.workpadConflict.redirectLabel', { + defaultMessage: 'Workpad', + }); + export const WorkpadPresentationHelper: FC = ({ children }) => { const platformService = usePlatformService(); const workpad = useSelector(getWorkpad); @@ -34,5 +39,19 @@ export const WorkpadPresentationHelper: FC = ({ children }) => { setDocTitle(workpad.name); }, [workpad.name]); - return <>{children}</>; + const conflictElement = workpad.aliasId + ? platformService.getLegacyUrlConflict?.({ + objectNoun: getWorkpadLabel(), + currentObjectId: workpad.id, + otherObjectId: workpad.aliasId, + otherObjectPath: `#/workpad/${workpad.aliasId}`, + }) + : null; + + return ( + <> + {conflictElement} + {children} + </> + ); }; diff --git a/x-pack/plugins/canvas/public/routes/workpad/workpad_route.tsx b/x-pack/plugins/canvas/public/routes/workpad/workpad_route.tsx index 2c1ad4fcb6aa1..c5374c3ad6e04 100644 --- a/x-pack/plugins/canvas/public/routes/workpad/workpad_route.tsx +++ b/x-pack/plugins/canvas/public/routes/workpad/workpad_route.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC, useEffect } from 'react'; +import React, { FC, useEffect, useCallback } from 'react'; import { Route, Switch, Redirect, useParams } from 'react-router-dom'; import { useDispatch } from 'react-redux'; import { WorkpadApp } from '../../components/workpad_app'; @@ -21,56 +21,93 @@ import { useRestoreHistory } from './hooks/use_restore_history'; import { useWorkpadHistory } from './hooks/use_workpad_history'; import { usePageSync } from './hooks/use_page_sync'; import { useWorkpadPersist } from './hooks/use_workpad_persist'; -import { WorkpadPageRouteProps, WorkpadRouteProps, WorkpadPageRouteParams } from '.'; +import { WorkpadRouteProps, WorkpadPageRouteParams } from '.'; import { WorkpadRoutingContextComponent } from './workpad_routing_context'; import { WorkpadPresentationHelper } from './workpad_presentation_helper'; const { workpadRoutes: strings } = ErrorStrings; -export const WorkpadRoute = () => ( - <Route - path={'/workpad/:id'} - exact={false} - children={(route: WorkpadRouteProps) => ( - <WorkpadLoaderComponent params={route.match.params} key="workpad-loader"> - {(workpad: CanvasWorkpad) => ( - <Switch> - <Route - path="/workpad/:id/page/:pageNumber" - children={(pageRoute) => ( - <WorkpadHistoryManager> - <WorkpadRoutingContextComponent> - <WorkpadPresentationHelper> - <WorkpadApp /> - </WorkpadPresentationHelper> - </WorkpadRoutingContextComponent> - </WorkpadHistoryManager> - )} - /> - <Route path="/workpad/:id" strict={false} exact={true}> - <Redirect to={`/workpad/${route.match.params.id}/page/${workpad.page + 1}`} /> - </Route> - </Switch> - )} - </WorkpadLoaderComponent> - )} - /> -); - -export const ExportWorkpadRoute = () => ( - <Route - path={'/export/workpad/pdf/:id/page/:pageNumber'} - children={(route: WorkpadPageRouteProps) => ( - <WorkpadLoaderComponent loadPages={false} params={route.match.params}> - {() => ( - <ExportRouteManager> - <ExportApp /> - </ExportRouteManager> - )} - </WorkpadLoaderComponent> - )} - /> -); +export const WorkpadRoute = () => { + return ( + <Route + path={['/workpad/:id/page/:pageNumber', '/workpad/:id']} + exact={false} + children={(route: WorkpadRouteProps) => { + return <WorkpadRouteComponent route={route} />; + }} + /> + ); +}; + +const WorkpadRouteComponent: FC<{ route: WorkpadRouteProps }> = ({ route }) => { + const getRedirectPath = useCallback( + (workpadId: string) => + `/workpad/${workpadId}${ + route.match.params.pageNumber ? `/page/${route.match.params.pageNumber}` : '' + }`, + [route.match.params.pageNumber] + ); + + return ( + <WorkpadLoaderComponent + params={route.match.params} + key="workpad-loader" + getRedirectPath={getRedirectPath} + > + {(workpad: CanvasWorkpad) => ( + <Switch> + <Route + path="/workpad/:id/page/:pageNumber" + children={(pageRoute) => ( + <WorkpadHistoryManager> + <WorkpadRoutingContextComponent> + <WorkpadPresentationHelper> + <WorkpadApp /> + </WorkpadPresentationHelper> + </WorkpadRoutingContextComponent> + </WorkpadHistoryManager> + )} + /> + <Route path="/workpad/:id" strict={false} exact={true}> + <Redirect to={`/workpad/${route.match.params.id}/page/${workpad.page + 1}`} /> + </Route> + </Switch> + )} + </WorkpadLoaderComponent> + ); +}; + +export const ExportWorkpadRoute = () => { + return ( + <Route + path={'/export/workpad/pdf/:id/page/:pageNumber'} + children={(route: WorkpadRouteProps) => { + return <ExportWorkpadRouteComponent route={route} />; + }} + /> + ); +}; + +const ExportWorkpadRouteComponent: FC<{ route: WorkpadRouteProps }> = ({ route: { match } }) => { + const getRedirectPath = useCallback( + (workpadId: string) => `/export/workpad/pdf/${workpadId}/page/${match.params.pageNumber}`, + [match.params.pageNumber] + ); + + return ( + <WorkpadLoaderComponent + loadPages={false} + params={match.params} + getRedirectPath={getRedirectPath} + > + {() => ( + <ExportRouteManager> + <ExportApp /> + </ExportRouteManager> + )} + </WorkpadLoaderComponent> + ); +}; export const ExportRouteManager: FC = ({ children }) => { const params = useParams<WorkpadPageRouteParams>(); @@ -97,9 +134,10 @@ export const WorkpadHistoryManager: FC = ({ children }) => { const WorkpadLoaderComponent: FC<{ params: WorkpadRouteProps['match']['params']; loadPages?: boolean; + getRedirectPath: (workpadId: string) => string; children: (workpad: CanvasWorkpad) => JSX.Element; -}> = ({ params, children, loadPages }) => { - const [workpad, error] = useWorkpad(params.id, loadPages); +}> = ({ params, children, loadPages, getRedirectPath }) => { + const [workpad, error] = useWorkpad(params.id, loadPages, getRedirectPath); const notifyService = useNotifyService(); useEffect(() => { diff --git a/x-pack/plugins/canvas/public/services/kibana/platform.ts b/x-pack/plugins/canvas/public/services/kibana/platform.ts index dc524aab6f444..aea8c6e7f7a95 100644 --- a/x-pack/plugins/canvas/public/services/kibana/platform.ts +++ b/x-pack/plugins/canvas/public/services/kibana/platform.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { KibanaPluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public'; import { CanvasStartDeps } from '../../plugin'; @@ -15,7 +14,11 @@ export type CanvaPlatformServiceFactory = KibanaPluginServiceFactory< CanvasStartDeps >; -export const platformServiceFactory: CanvaPlatformServiceFactory = ({ coreStart, initContext }) => { +export const platformServiceFactory: CanvaPlatformServiceFactory = ({ + coreStart, + initContext, + startPlugins, +}) => { if (!initContext) { throw new Error('Canvas platform service requires init context'); } @@ -34,6 +37,8 @@ export const platformServiceFactory: CanvaPlatformServiceFactory = ({ coreStart, setBreadcrumbs: coreStart.chrome.setBreadcrumbs, setRecentlyAccessed: coreStart.chrome.recentlyAccessed.add, setFullscreen: coreStart.chrome.setIsVisible, + redirectLegacyUrl: startPlugins.spaces?.ui.redirectLegacyUrl, + getLegacyUrlConflict: startPlugins.spaces?.ui.components.getLegacyUrlConflict, // TODO: these should go away. We want thin accessors, not entire objects. // Entire objects are hard to mock, and hide our dependency on the external service. diff --git a/x-pack/plugins/canvas/public/services/kibana/workpad.ts b/x-pack/plugins/canvas/public/services/kibana/workpad.ts index 8609d5055cb83..35b82735845d0 100644 --- a/x-pack/plugins/canvas/public/services/kibana/workpad.ts +++ b/x-pack/plugins/canvas/public/services/kibana/workpad.ts @@ -8,7 +8,7 @@ import { KibanaPluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public'; import { CanvasStartDeps } from '../../plugin'; -import { CanvasWorkpadService } from '../workpad'; +import { CanvasWorkpadService, ResolveWorkpadResponse } from '../workpad'; import { API_ROUTE_WORKPAD, @@ -67,6 +67,23 @@ export const workpadServiceFactory: CanvasWorkpadServiceFactory = ({ coreStart, return { css: DEFAULT_WORKPAD_CSS, variables: [], ...workpad }; }, + resolve: async (id: string) => { + const { workpad, outcome, aliasId } = await coreStart.http.get<ResolveWorkpadResponse>( + `${getApiPath()}/resolve/${id}` + ); + + return { + outcome, + aliasId, + workpad: { + // @ts-ignore: Shimming legacy workpads that might not have CSS + css: DEFAULT_WORKPAD_CSS, + // @ts-ignore: Shimming legacy workpads that might not have variables + variables: [], + ...workpad, + }, + }; + }, create: (workpad: CanvasWorkpad) => { return coreStart.http.post(getApiPath(), { body: JSON.stringify({ diff --git a/x-pack/plugins/canvas/public/services/platform.ts b/x-pack/plugins/canvas/public/services/platform.ts index 9bff61a0c668a..c476fac3b8789 100644 --- a/x-pack/plugins/canvas/public/services/platform.ts +++ b/x-pack/plugins/canvas/public/services/platform.ts @@ -15,6 +15,8 @@ import { ChromeStart, } from '../../../../../src/core/public'; +import { SpacesPluginStart } from '../../../spaces/public'; + export interface CanvasPlatformService { getBasePath: () => string; getBasePathInterface: () => IBasePath; @@ -27,6 +29,8 @@ export interface CanvasPlatformService { setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => void; setRecentlyAccessed: (link: string, label: string, id: string) => void; setFullscreen: ChromeStart['setIsVisible']; + redirectLegacyUrl?: SpacesPluginStart['ui']['redirectLegacyUrl']; + getLegacyUrlConflict?: SpacesPluginStart['ui']['components']['getLegacyUrlConflict']; // TODO: these should go away. We want thin accessors, not entire objects. // Entire objects are hard to mock, and hide our dependency on the external service. diff --git a/x-pack/plugins/canvas/public/services/storybook/workpad.ts b/x-pack/plugins/canvas/public/services/storybook/workpad.ts index e4d176f490071..6c77bdb1adeac 100644 --- a/x-pack/plugins/canvas/public/services/storybook/workpad.ts +++ b/x-pack/plugins/canvas/public/services/storybook/workpad.ts @@ -77,6 +77,10 @@ export const workpadServiceFactory: CanvasWorkpadServiceFactory = ({ action('workpadService.get')(id); return Promise.resolve({ ...getDefaultWorkpad(), id }); }, + resolve: (id: string) => { + action('workpadService.resolve')(id); + return Promise.resolve({ outcome: 'exactMatch', workpad: { ...getDefaultWorkpad(), id } }); + }, findTemplates: () => { action('workpadService.findTemplates')(); return (hasTemplates ? findSomeTemplates() : findNoTemplates())(); diff --git a/x-pack/plugins/canvas/public/services/stubs/platform.ts b/x-pack/plugins/canvas/public/services/stubs/platform.ts index 3942fcf145cee..71a252028a242 100644 --- a/x-pack/plugins/canvas/public/services/stubs/platform.ts +++ b/x-pack/plugins/canvas/public/services/stubs/platform.ts @@ -34,4 +34,6 @@ export const platformServiceFactory: CanvasPlatformServiceFactory = () => ({ getSavedObjectsClient: noop, getUISettings: noop, setFullscreen: noop, + redirectLegacyUrl: noop, + getLegacyUrlConflict: undefined, }); diff --git a/x-pack/plugins/canvas/public/services/stubs/workpad.ts b/x-pack/plugins/canvas/public/services/stubs/workpad.ts index 19979e51b5e46..c10244038750d 100644 --- a/x-pack/plugins/canvas/public/services/stubs/workpad.ts +++ b/x-pack/plugins/canvas/public/services/stubs/workpad.ts @@ -99,6 +99,8 @@ export const getSomeTemplates = () => ({ templates }); export const workpadServiceFactory: CanvasWorkpadServiceFactory = () => ({ get: (id: string) => Promise.resolve({ ...getDefaultWorkpad(), id }), + resolve: (id: string) => + Promise.resolve({ outcome: 'exactMatch', workpad: { ...getDefaultWorkpad(), id } }), findTemplates: findNoTemplates(), create: (workpad) => Promise.resolve(workpad), createFromTemplate: (_templateId: string) => Promise.resolve(getDefaultWorkpad()), diff --git a/x-pack/plugins/canvas/public/services/workpad.ts b/x-pack/plugins/canvas/public/services/workpad.ts index c0e948669647c..8e77ab3f321ef 100644 --- a/x-pack/plugins/canvas/public/services/workpad.ts +++ b/x-pack/plugins/canvas/public/services/workpad.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { SavedObjectsResolveResponse } from 'src/core/public'; import { CanvasWorkpad, CanvasTemplate } from '../../types'; import { CanvasRenderedWorkpad } from '../../shareable_runtime/types'; @@ -18,8 +19,15 @@ export interface WorkpadFindResponse { export interface TemplateFindResponse { templates: CanvasTemplate[]; } + +export interface ResolveWorkpadResponse { + workpad: CanvasWorkpad; + outcome: SavedObjectsResolveResponse['outcome']; + aliasId?: SavedObjectsResolveResponse['alias_target_id']; +} export interface CanvasWorkpadService { get: (id: string) => Promise<CanvasWorkpad>; + resolve: (id: string) => Promise<ResolveWorkpadResponse>; create: (workpad: CanvasWorkpad) => Promise<CanvasWorkpad>; createFromTemplate: (templateId: string) => Promise<CanvasWorkpad>; find: (term: string) => Promise<WorkpadFindResponse>; diff --git a/x-pack/plugins/canvas/public/style/main.scss b/x-pack/plugins/canvas/public/style/main.scss index 4f293e9072e4f..b53181bef85b9 100644 --- a/x-pack/plugins/canvas/public/style/main.scss +++ b/x-pack/plugins/canvas/public/style/main.scss @@ -11,6 +11,7 @@ $canvasElementCardWidth: 210px; .canvas.canvasContainer { display: flex; flex-grow: 1; + flex-direction: column; background-color: $euiPageBackgroundColor; } diff --git a/x-pack/plugins/canvas/server/mocks/workpad_route_context.ts b/x-pack/plugins/canvas/server/mocks/workpad_route_context.ts index abba97639a4c9..216cdc0970dc4 100644 --- a/x-pack/plugins/canvas/server/mocks/workpad_route_context.ts +++ b/x-pack/plugins/canvas/server/mocks/workpad_route_context.ts @@ -13,6 +13,7 @@ export interface MockWorkpadRouteContext extends CanvasRouteHandlerContext { create: jest.Mock; get: jest.Mock; update: jest.Mock; + resolve: jest.Mock; }; }; } @@ -23,6 +24,7 @@ export const workpadRouteContextMock = { create: jest.fn(), get: jest.fn(), update: jest.fn(), + resolve: jest.fn(), }, }), }; diff --git a/x-pack/plugins/canvas/server/routes/workpad/get.ts b/x-pack/plugins/canvas/server/routes/workpad/get.ts index ff3ed4bad55b9..11e39adc4ca32 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/get.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/get.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { RouteInitializerDeps } from '../'; import { API_ROUTE_WORKPAD } from '../../../common/lib/constants'; import { catchErrorHandler } from '../catch_error_handler'; +import { shimWorkpad } from './shim_workpad'; export function initializeGetWorkpadRoute(deps: RouteInitializerDeps) { const { router } = deps; @@ -24,24 +25,7 @@ export function initializeGetWorkpadRoute(deps: RouteInitializerDeps) { catchErrorHandler(async (context, request, response) => { const workpad = await context.canvas.workpad.get(request.params.id); - if ( - // not sure if we need to be this defensive - workpad.type === 'canvas-workpad' && - workpad.attributes && - workpad.attributes.pages && - workpad.attributes.pages.length - ) { - workpad.attributes.pages.forEach((page) => { - const elements = (page.elements || []).filter( - ({ id: pageId }) => !pageId.startsWith('group') - ); - const groups = (page.groups || []).concat( - (page.elements || []).filter(({ id: pageId }) => pageId.startsWith('group')) - ); - page.elements = elements; - page.groups = groups; - }); - } + shimWorkpad(workpad); return response.ok({ body: { diff --git a/x-pack/plugins/canvas/server/routes/workpad/index.ts b/x-pack/plugins/canvas/server/routes/workpad/index.ts index 6a7a080047b91..8483642e59c5a 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/index.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/index.ts @@ -11,9 +11,11 @@ import { initializeGetWorkpadRoute } from './get'; import { initializeCreateWorkpadRoute } from './create'; import { initializeUpdateWorkpadRoute, initializeUpdateWorkpadAssetsRoute } from './update'; import { initializeDeleteWorkpadRoute } from './delete'; +import { initializeResolveWorkpadRoute } from './resolve'; export function initWorkpadRoutes(deps: RouteInitializerDeps) { initializeFindWorkpadsRoute(deps); + initializeResolveWorkpadRoute(deps); initializeGetWorkpadRoute(deps); initializeCreateWorkpadRoute(deps); initializeUpdateWorkpadRoute(deps); diff --git a/x-pack/plugins/canvas/server/routes/workpad/resolve.test.ts b/x-pack/plugins/canvas/server/routes/workpad/resolve.test.ts new file mode 100644 index 0000000000000..bc93e115e137f --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/resolve.test.ts @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CANVAS_TYPE } from '../../../common/lib/constants'; +import { initializeResolveWorkpadRoute } from './resolve'; +import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; +import { savedObjectsClientMock, httpServerMock } from 'src/core/server/mocks'; +import { workpadWithGroupAsElement } from '../../../__fixtures__/workpads'; +import { CanvasWorkpad } from '../../../types'; +import { getMockedRouterDeps } from '../test_helpers'; +import { workpadRouteContextMock, MockWorkpadRouteContext } from '../../mocks'; + +const mockRouteContext = { + canvas: workpadRouteContextMock.create(), +} as unknown as MockWorkpadRouteContext; + +describe('RESOLVE workpad', () => { + let routeHandler: RequestHandler<any, any, any>; + + beforeEach(() => { + const routerDeps = getMockedRouterDeps(); + initializeResolveWorkpadRoute(routerDeps); + + routeHandler = routerDeps.router.get.mock.calls[0][1]; + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it(`returns 200 when the workpad is found`, async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: 'api/canvas/workpad/resolve/123', + params: { + id: '123', + }, + }); + + const outcome = 'aliasMatch'; + const aliasId = 'alias-id'; + + mockRouteContext.canvas.workpad.resolve.mockResolvedValue({ + saved_object: { + id: '123', + type: CANVAS_TYPE, + attributes: { foo: true }, + references: [], + }, + outcome, + alias_target_id: aliasId, + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.status).toBe(200); + expect(response.payload).toMatchInlineSnapshot(` + Object { + "aliasId": "alias-id", + "outcome": "aliasMatch", + "workpad": Object { + "foo": true, + "id": "123", + }, + } + `); + + expect(mockRouteContext.canvas.workpad.resolve.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "123", + ], + ] + `); + }); + + it('corrects elements that should be groups', async () => { + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: 'api/canvas/workpad/resolve/123', + params: { + id: '123', + }, + }); + + mockRouteContext.canvas.workpad.resolve.mockResolvedValue({ + saved_object: { + id: '123', + type: CANVAS_TYPE, + attributes: workpadWithGroupAsElement as any, + references: [], + }, + outcome: 'exactMatch', + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + const workpad = response.payload.workpad as CanvasWorkpad; + + expect(response.status).toBe(200); + expect(workpad).not.toBeUndefined(); + + expect(workpad.pages[0].elements.length).toBe(1); + expect(workpad.pages[0].groups.length).toBe(1); + }); + + it('returns 404 if the workpad is not found', async () => { + const id = '123'; + const request = httpServerMock.createKibanaRequest({ + method: 'get', + path: 'api/canvas/workpad/resolve/123', + params: { + id, + }, + }); + + const savedObjectsClient = savedObjectsClientMock.create(); + mockRouteContext.canvas.workpad.resolve.mockImplementation(() => { + throw savedObjectsClient.errors.createGenericNotFoundError(CANVAS_TYPE, id); + }); + + const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); + + expect(response.payload).toMatchInlineSnapshot(` + Object { + "error": "Not Found", + "message": "Saved object [canvas-workpad/123] not found", + "statusCode": 404, + } + `); + }); +}); diff --git a/x-pack/plugins/canvas/server/routes/workpad/resolve.ts b/x-pack/plugins/canvas/server/routes/workpad/resolve.ts new file mode 100644 index 0000000000000..7c21ecf9ed055 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/resolve.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 { schema } from '@kbn/config-schema'; +import { RouteInitializerDeps } from '../'; +import { API_ROUTE_WORKPAD } from '../../../common/lib/constants'; +import { catchErrorHandler } from '../catch_error_handler'; +import { shimWorkpad } from './shim_workpad'; + +export function initializeResolveWorkpadRoute(deps: RouteInitializerDeps) { + const { router } = deps; + router.get( + { + path: `${API_ROUTE_WORKPAD}/resolve/{id}`, + validate: { + params: schema.object({ + id: schema.string(), + }), + }, + }, + catchErrorHandler(async (context, request, response) => { + const resolved = await context.canvas.workpad.resolve(request.params.id); + const { saved_object: workpad } = resolved; + + shimWorkpad(workpad); + + return response.ok({ + body: { + workpad: { + id: workpad.id, + ...workpad.attributes, + }, + outcome: resolved.outcome, + aliasId: resolved.alias_target_id, + }, + }); + }) + ); +} diff --git a/x-pack/plugins/canvas/server/routes/workpad/shim_workpad.ts b/x-pack/plugins/canvas/server/routes/workpad/shim_workpad.ts new file mode 100644 index 0000000000000..63fa5bb252985 --- /dev/null +++ b/x-pack/plugins/canvas/server/routes/workpad/shim_workpad.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObject } from 'kibana/server'; +import { WorkpadAttributes } from './workpad_attributes'; + +export function shimWorkpad(workpad: SavedObject<WorkpadAttributes>) { + if ( + // not sure if we need to be this defensive + workpad.type === 'canvas-workpad' && + workpad.attributes && + workpad.attributes.pages && + workpad.attributes.pages.length + ) { + workpad.attributes.pages.forEach((page) => { + const elements = (page.elements || []).filter( + ({ id: pageId }) => !pageId.startsWith('group') + ); + const groups = (page.groups || []).concat( + (page.elements || []).filter(({ id: pageId }) => pageId.startsWith('group')) + ); + page.elements = elements; + page.groups = groups; + }); + } +} diff --git a/x-pack/plugins/canvas/server/workpad_route_context.test.ts b/x-pack/plugins/canvas/server/workpad_route_context.test.ts index d13b8aa9eb634..6496edf1a0e12 100644 --- a/x-pack/plugins/canvas/server/workpad_route_context.test.ts +++ b/x-pack/plugins/canvas/server/workpad_route_context.test.ts @@ -141,6 +141,31 @@ describe('workpad route context', () => { }); }); + describe('RESOLVE', () => { + it('injects references to the saved object', async () => { + const id = 'so-id'; + const canvasContext = await workpadRouteContext( + mockContext, + undefined as any, + undefined as any + ); + + (mockContext.core.savedObjects.client.resolve as jest.Mock).mockResolvedValue({ + saved_object: { attributes: extractedWorkpad, references }, + outcome: 'exactMatch', + }); + + mockedExpressionService.inject.mockReturnValue(fromExpression(injectedExpression)); + + const result = await canvasContext.workpad.resolve(id); + const { id: ingnoredId, ...expectedAttributes } = injectedWorkpad; + + expect(mockContext.core.savedObjects.client.resolve).toBeCalledWith(CANVAS_TYPE, id); + + expect(result.saved_object.attributes).toEqual(expectedAttributes); + }); + }); + describe('UPDATE', () => { it('extracts from the given attributes', async () => { const id = 'workpad-id'; diff --git a/x-pack/plugins/canvas/server/workpad_route_context.ts b/x-pack/plugins/canvas/server/workpad_route_context.ts index 5689bf9961f76..9727327fcbd79 100644 --- a/x-pack/plugins/canvas/server/workpad_route_context.ts +++ b/x-pack/plugins/canvas/server/workpad_route_context.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { RequestHandlerContext, RequestHandlerContextProvider, SavedObject } from 'kibana/server'; +import { + RequestHandlerContext, + RequestHandlerContextProvider, + SavedObject, + SavedObjectsResolveResponse, +} from 'kibana/server'; import { ExpressionsService } from 'src/plugins/expressions'; import { WorkpadAttributes } from './routes/workpad/workpad_attributes'; import { CANVAS_TYPE } from '../common/lib/constants'; @@ -18,6 +23,7 @@ export interface CanvasRouteHandlerContext extends RequestHandlerContext { workpad: { create: (attributes: CanvasWorkpad) => Promise<SavedObject<WorkpadAttributes>>; get: (id: string) => Promise<SavedObject<WorkpadAttributes>>; + resolve: (id: string) => Promise<SavedObjectsResolveResponse<WorkpadAttributes>>; update: ( id: string, attributes: Partial<CanvasWorkpad> @@ -66,6 +72,20 @@ export const createWorkpadRouteContext: ( return workpad; }, + resolve: async (id: string) => { + const resolved = await context.core.savedObjects.client.resolve<WorkpadAttributes>( + CANVAS_TYPE, + id + ); + + resolved.saved_object.attributes = injectReferences( + resolved.saved_object.attributes, + resolved.saved_object.references, + expressions + ); + + return resolved; + }, update: async (id: string, { id: omittedId, ...workpad }: Partial<CanvasWorkpad>) => { const now = new Date().toISOString(); diff --git a/x-pack/plugins/canvas/tsconfig.json b/x-pack/plugins/canvas/tsconfig.json index 5a5a1883240b7..5e3c7a7299adb 100644 --- a/x-pack/plugins/canvas/tsconfig.json +++ b/x-pack/plugins/canvas/tsconfig.json @@ -49,6 +49,7 @@ { "path": "../features/tsconfig.json" }, { "path": "../lens/tsconfig.json" }, { "path": "../maps/tsconfig.json" }, - { "path": "../reporting/tsconfig.json" } + { "path": "../reporting/tsconfig.json" }, + { "path": "../spaces/tsconfig.json" }, ] } diff --git a/x-pack/plugins/canvas/types/canvas.ts b/x-pack/plugins/canvas/types/canvas.ts index d3799cf59745a..0868054d0a489 100644 --- a/x-pack/plugins/canvas/types/canvas.ts +++ b/x-pack/plugins/canvas/types/canvas.ts @@ -53,6 +53,7 @@ export interface CanvasWorkpad { variables: CanvasVariable[]; height: number; id: string; + aliasId?: string; isWriteable: boolean; name: string; page: number; diff --git a/x-pack/plugins/event_log/server/event_log_service.mock.ts b/x-pack/plugins/event_log/server/event_log_service.mock.ts index a6e43a5b488c7..a3ad81eb0e5a6 100644 --- a/x-pack/plugins/event_log/server/event_log_service.mock.ts +++ b/x-pack/plugins/event_log/server/event_log_service.mock.ts @@ -10,7 +10,6 @@ import { eventLoggerMock } from './event_logger.mock'; const createEventLogServiceMock = () => { const mock: jest.Mocked<IEventLogService> = { - isEnabled: jest.fn(), isLoggingEntries: jest.fn(), isIndexingEntries: jest.fn(), registerProviderActions: jest.fn(), diff --git a/x-pack/plugins/event_log/server/event_log_service.test.ts b/x-pack/plugins/event_log/server/event_log_service.test.ts index ad8b8c06b47f3..aad51a03b2f06 100644 --- a/x-pack/plugins/event_log/server/event_log_service.test.ts +++ b/x-pack/plugins/event_log/server/event_log_service.test.ts @@ -19,14 +19,13 @@ describe('EventLogService', () => { const esContext = contextMock.create(); function getService(config: IEventLogConfig) { - const { enabled, logEntries, indexEntries } = config; + const { logEntries, indexEntries } = config; return new EventLogService({ esContext, systemLogger, kibanaUUID: '42', savedObjectProviderRegistry, config: { - enabled, logEntries, indexEntries, }, @@ -37,30 +36,19 @@ describe('EventLogService', () => { test('returns config values from service methods', () => { let service; - service = getService({ enabled: true, logEntries: true, indexEntries: true }); - expect(service.isEnabled()).toEqual(true); + service = getService({ logEntries: true, indexEntries: true }); expect(service.isLoggingEntries()).toEqual(true); expect(service.isIndexingEntries()).toEqual(true); - service = getService({ enabled: true, logEntries: false, indexEntries: true }); - expect(service.isEnabled()).toEqual(true); + service = getService({ logEntries: false, indexEntries: true }); expect(service.isLoggingEntries()).toEqual(false); expect(service.isIndexingEntries()).toEqual(true); - service = getService({ enabled: true, logEntries: true, indexEntries: false }); - expect(service.isEnabled()).toEqual(true); + service = getService({ logEntries: true, indexEntries: false }); expect(service.isLoggingEntries()).toEqual(true); expect(service.isIndexingEntries()).toEqual(false); - service = getService({ enabled: true, logEntries: false, indexEntries: false }); - expect(service.isEnabled()).toEqual(true); - expect(service.isLoggingEntries()).toEqual(false); - expect(service.isIndexingEntries()).toEqual(false); - - // this is the only non-obvious one; when enabled is false, - // logging/indexing will be false as well. - service = getService({ enabled: false, logEntries: true, indexEntries: true }); - expect(service.isEnabled()).toEqual(false); + service = getService({ logEntries: false, indexEntries: false }); expect(service.isLoggingEntries()).toEqual(false); expect(service.isIndexingEntries()).toEqual(false); }); diff --git a/x-pack/plugins/event_log/server/event_log_service.ts b/x-pack/plugins/event_log/server/event_log_service.ts index f6e1533aa1155..993631ed3ca8a 100644 --- a/x-pack/plugins/event_log/server/event_log_service.ts +++ b/x-pack/plugins/event_log/server/event_log_service.ts @@ -55,16 +55,12 @@ export class EventLogService implements IEventLogService { this.kibanaVersion = kibanaVersion; } - public isEnabled(): boolean { - return this.config.enabled; - } - public isLoggingEntries(): boolean { - return this.isEnabled() && this.config.logEntries; + return this.config.logEntries; } public isIndexingEntries(): boolean { - return this.isEnabled() && this.config.indexEntries; + return this.config.indexEntries; } registerProviderActions(provider: string, actions: string[]): void { diff --git a/x-pack/plugins/event_log/server/event_logger.test.ts b/x-pack/plugins/event_log/server/event_logger.test.ts index d90fd93c60043..43d791a18b5fc 100644 --- a/x-pack/plugins/event_log/server/event_logger.test.ts +++ b/x-pack/plugins/event_log/server/event_logger.test.ts @@ -31,7 +31,7 @@ describe('EventLogger', () => { service = new EventLogService({ esContext, systemLogger, - config: { enabled: true, logEntries: true, indexEntries: true }, + config: { logEntries: true, indexEntries: true }, kibanaUUID: KIBANA_SERVER_UUID, savedObjectProviderRegistry: savedObjectProviderRegistryMock.create(), kibanaVersion: '1.0.1', diff --git a/x-pack/plugins/event_log/server/event_logger.ts b/x-pack/plugins/event_log/server/event_logger.ts index bcda73da215ae..97335f9cda8d1 100644 --- a/x-pack/plugins/event_log/server/event_logger.ts +++ b/x-pack/plugins/event_log/server/event_logger.ts @@ -66,8 +66,6 @@ export class EventLogger implements IEventLogger { // non-blocking, but spawns an async task to do the work logEvent(eventProperties: IEvent): void { - if (!this.eventLogService.isEnabled()) return; - const event: IEvent = {}; const fixedProperties = { ecs: { diff --git a/x-pack/plugins/event_log/server/index.ts b/x-pack/plugins/event_log/server/index.ts index deeee970ce68a..14c121664d4a8 100644 --- a/x-pack/plugins/event_log/server/index.ts +++ b/x-pack/plugins/event_log/server/index.ts @@ -26,20 +26,5 @@ export { createReadySignal } from './lib/ready_signal'; export const config: PluginConfigDescriptor<IEventLogConfig> = { schema: ConfigSchema, - deprecations: () => [ - (settings, fromPath, addDeprecation) => { - if ( - settings?.xpack?.eventLog?.enabled === false || - settings?.xpack?.eventLog?.enabled === true - ) { - addDeprecation({ - message: `"xpack.eventLog.enabled" is deprecated. The ability to disable this plugin will be removed in 8.0.0.`, - correctiveActions: { - manualSteps: [`Remove "xpack.eventLog.enabled" from your kibana configs.`], - }, - }); - } - }, - ], }; export const plugin = (context: PluginInitializerContext) => new Plugin(context); diff --git a/x-pack/plugins/event_log/server/plugin.test.ts b/x-pack/plugins/event_log/server/plugin.test.ts index 166b084deb6bf..fa66d4b0b02b3 100644 --- a/x-pack/plugins/event_log/server/plugin.test.ts +++ b/x-pack/plugins/event_log/server/plugin.test.ts @@ -21,7 +21,6 @@ describe('event_log plugin', () => { const setup = plugin.setup(coreSetup); expect(typeof setup.getLogger).toBe('function'); expect(typeof setup.getProviderActions).toBe('function'); - expect(typeof setup.isEnabled).toBe('function'); expect(typeof setup.isIndexingEntries).toBe('function'); expect(typeof setup.isLoggingEntries).toBe('function'); expect(typeof setup.isProviderActionRegistered).toBe('function'); diff --git a/x-pack/plugins/event_log/server/types.ts b/x-pack/plugins/event_log/server/types.ts index 0750e89473b8e..c50bed7e01dd5 100644 --- a/x-pack/plugins/event_log/server/types.ts +++ b/x-pack/plugins/event_log/server/types.ts @@ -18,7 +18,6 @@ import { SavedObjectProvider } from './saved_object_provider_registry'; export const SAVED_OBJECT_REL_PRIMARY = 'primary'; export const ConfigSchema = schema.object({ - enabled: schema.boolean({ defaultValue: true }), logEntries: schema.boolean({ defaultValue: false }), indexEntries: schema.boolean({ defaultValue: true }), }); @@ -27,7 +26,6 @@ export type IEventLogConfig = TypeOf<typeof ConfigSchema>; // the object exposed by plugin.setup() export interface IEventLogService { - isEnabled(): boolean; isLoggingEntries(): boolean; isIndexingEntries(): boolean; registerProviderActions(provider: string, actions: string[]): void; diff --git a/x-pack/plugins/fleet/common/services/routes.ts b/x-pack/plugins/fleet/common/services/routes.ts index 5294c31d6a289..79ea19360c849 100644 --- a/x-pack/plugins/fleet/common/services/routes.ts +++ b/x-pack/plugins/fleet/common/services/routes.ts @@ -59,6 +59,10 @@ export const epmRouteService = { getRemovePath: (pkgkey: string) => { return EPM_API_ROUTES.DELETE_PATTERN.replace('{pkgkey}', pkgkey).replace(/\/$/, ''); // trim trailing slash }, + + getUpdatePath: (pkgkey: string) => { + return EPM_API_ROUTES.INFO_PATTERN.replace('{pkgkey}', pkgkey); + }, }; export const packagePolicyRouteService = { diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 06e3d13c2394b..20f41174a9847 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -351,6 +351,7 @@ export interface EpmPackageAdditions { assets: AssetsGroupedByServiceByType; removable?: boolean; notice?: string; + keepPoliciesUpToDate?: boolean; } type Merge<FirstType, SecondType> = Omit<FirstType, Extract<keyof FirstType, keyof SecondType>> & @@ -364,7 +365,7 @@ export type PackageListItem = Installable<RegistrySearchResult> & { }; export interface IntegrationCardItem { - uiInternalPathUrl: string; + url: string; release?: 'beta' | 'experimental' | 'ga'; description: string; name: string; @@ -391,6 +392,7 @@ export interface Installation extends SavedObjectAttributes { install_version: string; install_started_at: string; install_source: InstallSource; + keep_policies_up_to_date: boolean; } export interface PackageUsageStats { diff --git a/x-pack/plugins/fleet/common/types/rest_spec/epm.ts b/x-pack/plugins/fleet/common/types/rest_spec/epm.ts index 51772eadca69e..cfe0b4abdcd3c 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/epm.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/epm.ts @@ -57,6 +57,19 @@ export interface GetInfoResponse { response: PackageInfo; } +export interface UpdatePackageRequest { + params: { + pkgkey: string; + }; + body: { + keepPoliciesUpToDate?: boolean; + }; +} + +export interface UpdatePackageResponse { + response: PackageInfo; +} + export interface GetStatsRequest { params: { pkgname: string; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_cloud_instructions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_cloud_instructions.tsx index 88590ce3ce504..e2db1534597e0 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_cloud_instructions.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_cloud_instructions.tsx @@ -60,7 +60,7 @@ export const CloudInstructions: React.FC<{ deploymentUrl: string }> = ({ deploym > <FormattedMessage id="xpack.fleet.settings.userGuideLink" - defaultMessage="Fleet User Guide" + defaultMessage="Fleet and Elastic Agent Guide" /> </EuiLink> ), diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx index a8cab77af447c..5005c029a7588 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx @@ -185,7 +185,7 @@ export const FleetServerCommandStep = ({ > <FormattedMessage id="xpack.fleet.fleetServerSetup.setupGuideLink" - defaultMessage="Fleet User Guide" + defaultMessage="Fleet and Elastic Agent Guide" /> </EuiLink> ), @@ -755,7 +755,7 @@ export const OnPremInstructions: React.FC = () => { > <FormattedMessage id="xpack.fleet.fleetServerSetup.setupGuideLink" - defaultMessage="Fleet User Guide" + defaultMessage="Fleet and Elastic Agent Guide" /> </EuiLink> ), diff --git a/x-pack/plugins/fleet/public/applications/integrations/app.tsx b/x-pack/plugins/fleet/public/applications/integrations/app.tsx index d37cbe4c166df..c5cc1e1892eda 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/app.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/app.tsx @@ -201,13 +201,15 @@ export const IntegrationsAppContext: React.FC<{ <EuiThemeProvider darkMode={isDarkMode}> <UIExtensionsContext.Provider value={extensions}> <FleetStatusProvider> - <Router history={history}> - <AgentPolicyContextProvider> - <PackageInstallProvider notifications={startServices.notifications}> - {children} - </PackageInstallProvider> - </AgentPolicyContextProvider> - </Router> + <startServices.customIntegrations.ContextProvider> + <Router history={history}> + <AgentPolicyContextProvider> + <PackageInstallProvider notifications={startServices.notifications}> + {children} + </PackageInstallProvider> + </AgentPolicyContextProvider> + </Router> + </startServices.customIntegrations.ContextProvider> </FleetStatusProvider> </UIExtensionsContext.Provider> </EuiThemeProvider> diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.stories.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.stories.tsx index 69c70bba5be1d..bddbc4f027b4f 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.stories.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.stories.tsx @@ -29,7 +29,7 @@ const args: Args = { release: 'ga', id: 'id', version: '1.0.0', - uiInternalPathUrl: '/', + url: '/', icons: [], integration: '', }; @@ -66,6 +66,7 @@ export const Installed = ({ width, ...props }: Args) => { install_status: 'installed', install_source: 'registry', install_started_at: '2020-01-01T00:00:00.000Z', + keep_policies_up_to_date: false, }, references: [], }; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx index 8c7cd47e950f0..a68499dbd8dd0 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx @@ -29,7 +29,7 @@ export function PackageCard({ version, icons, integration, - uiInternalPathUrl, + url, release, }: PackageCardProps) { const betaBadgeLabel = release && release !== 'ga' ? RELEASE_BADGE_LABEL[release] : undefined; @@ -49,9 +49,10 @@ export function PackageCard({ size="xl" /> } - href={uiInternalPathUrl} + href={url} betaBadgeLabel={betaBadgeLabel} betaBadgeTooltipContent={betaBadgeLabelTooltipContent} + target={url.startsWith('http') || url.startsWith('https') ? '_blank' : undefined} /> ); } diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.stories.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.stories.tsx index f3bf7106fabcf..8349ec90ce3ba 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.stories.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.stories.tsx @@ -44,6 +44,7 @@ const savedObject: SavedObject<Installation> = { install_status: 'installed', install_source: 'registry', install_started_at: '2020-01-01T00:00:00.000Z', + keep_policies_up_to_date: false, }, references: [], }; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/index.tsx index 8424fecad08cd..8716d78dfb7bd 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/index.tsx @@ -7,3 +7,4 @@ export { UpdateIcon } from './update_icon'; export { IntegrationAgentPolicyCount } from './integration_agent_policy_count'; export { IconPanel, LoadingIconPanel } from './icon_panel'; +export { KeepPoliciesUpToDateSwitch } from './keep_policies_up_to_date_switch'; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/keep_policies_up_to_date_switch.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/keep_policies_up_to_date_switch.tsx new file mode 100644 index 0000000000000..751282cc42288 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/components/keep_policies_up_to_date_switch.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiSwitch, EuiSpacer, EuiText, EuiFlexGroup, EuiFlexItem, EuiIcon } from '@elastic/eui'; + +interface Props { + checked: boolean; + onChange: () => void; +} + +export const KeepPoliciesUpToDateSwitch: React.FunctionComponent<Props> = ({ + checked, + onChange, +}) => ( + <> + <EuiSwitch + label={i18n.translate( + 'xpack.fleet.integrations.settings.keepIntegrationPoliciesUpToDateLabel', + { defaultMessage: 'Keep integration policies up to date automatically' } + )} + checked={checked} + onChange={onChange} + /> + <EuiSpacer size="s" /> + <EuiText color="subdued" size="xs"> + <EuiFlexGroup alignItems="center" gutterSize="none"> + <EuiFlexItem grow={false}> + <EuiIcon type="iInCircle" /> + </EuiFlexItem> + <EuiFlexItem grow={false}> + <FormattedMessage + id="xpack.fleet.integrations.settings.keepIntegrationPoliciesUpToDateDescription" + defaultMessage="When enabled, Fleet will attempt to upgrade and deploy integration policies automatically" + /> + </EuiFlexItem> + </EuiFlexGroup> + </EuiText> + </> +); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/details.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/details.tsx index 467dae12fa583..5ef06e734f580 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/details.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/details.tsx @@ -18,6 +18,10 @@ import { } from '@elastic/eui'; import type { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list'; +import styled, { useTheme } from 'styled-components'; + +import type { EuiTheme } from '../../../../../../../../../../../src/plugins/kibana_react/common'; + import type { PackageInfo, PackageSpecCategory, @@ -28,13 +32,21 @@ import { entries } from '../../../../../types'; import { useGetCategories } from '../../../../../hooks'; import { AssetTitleMap, DisplayedAssets, ServiceTitleMap } from '../../../constants'; +import { + withSuspense, + LazyReplacementCard, +} from '../../../../../../../../../../../src/plugins/custom_integrations/public'; + import { NoticeModal } from './notice_modal'; +const ReplacementCard = withSuspense(LazyReplacementCard); + interface Props { packageInfo: PackageInfo; } export const Details: React.FC<Props> = memo(({ packageInfo }) => { + const theme = useTheme() as EuiTheme; const { data: categoriesData, isLoading: isLoadingCategories } = useGetCategories(); const packageCategories: string[] = useMemo(() => { if (!isLoadingCategories && categoriesData && categoriesData.response) { @@ -163,6 +175,23 @@ export const Details: React.FC<Props> = memo(({ packageInfo }) => { toggleNoticeModal, ]); + const Replacements = styled(EuiFlexItem)` + margin: 0; + + & .euiAccordion { + padding-top: ${parseInt(theme.eui.euiSizeL, 10) * 2}px; + + &::before { + content: ''; + display: block; + border-top: 1px solid ${theme.eui.euiColorLightShade}; + position: relative; + top: -${theme.eui.euiSizeL}; + margin: 0 ${theme.eui.euiSizeXS}; + } + } + `; + return ( <> <EuiPortal> @@ -181,6 +210,9 @@ export const Details: React.FC<Props> = memo(({ packageInfo }) => { <EuiFlexItem> <EuiDescriptionList type="column" compressed listItems={listItems} /> </EuiFlexItem> + <Replacements> + <ReplacementCard eprPackageName={packageInfo.name} /> + </Replacements> </EuiFlexGroup> </> ); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx index 07c95e0d77ec7..185ae10bcafd2 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/settings.tsx @@ -5,10 +5,11 @@ * 2.0. */ -import React, { memo, useEffect, useMemo, useState } from 'react'; +import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; import semverLt from 'semver/functions/lt'; +import { uniq } from 'lodash'; import { EuiCallOut, @@ -29,8 +30,16 @@ import { useGetPackageInstallStatus, useLink, sendUpgradePackagePolicyDryRun, + sendUpdatePackage, + useStartServices, } from '../../../../../hooks'; -import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants'; +import { + PACKAGE_POLICY_SAVED_OBJECT_TYPE, + AUTO_UPDATE_PACKAGES, + DEFAULT_PACKAGES, +} from '../../../../../constants'; + +import { KeepPoliciesUpToDateSwitch } from '../components'; import { InstallButton } from './install_button'; import { UpdateButton } from './update_button'; @@ -85,7 +94,7 @@ interface Props { } export const SettingsPage: React.FC<Props> = memo(({ packageInfo }: Props) => { - const { name, title, removable, latestVersion, version } = packageInfo; + const { name, title, removable, latestVersion, version, keepPoliciesUpToDate } = packageInfo; const [dryRunData, setDryRunData] = useState<UpgradePackagePolicyDryRunResponse | null>(); const [isUpgradingPackagePolicies, setIsUpgradingPackagePolicies] = useState<boolean>(false); const getPackageInstallStatus = useGetPackageInstallStatus(); @@ -95,6 +104,67 @@ export const SettingsPage: React.FC<Props> = memo(({ packageInfo }: Props) => { kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${name}`, }); + const { notifications } = useStartServices(); + + const shouldShowKeepPoliciesUpToDateSwitch = useMemo(() => { + const packages = [...DEFAULT_PACKAGES, ...AUTO_UPDATE_PACKAGES]; + + const packageNames = uniq(packages.map((pkg) => pkg.name)); + + return packageNames.includes(name); + }, [name]); + + const [keepPoliciesUpToDateSwitchValue, setKeepPoliciesUpToDateSwitchValue] = useState<boolean>( + keepPoliciesUpToDate ?? false + ); + + const handleKeepPoliciesUpToDateSwitchChange = useCallback(() => { + const saveKeepPoliciesUpToDate = async () => { + try { + setKeepPoliciesUpToDateSwitchValue((prev) => !prev); + + await sendUpdatePackage(`${packageInfo.name}-${packageInfo.version}`, { + keepPoliciesUpToDate: !keepPoliciesUpToDateSwitchValue, + }); + + notifications.toasts.addSuccess({ + title: i18n.translate('xpack.fleet.integrations.integrationSaved', { + defaultMessage: 'Integration settings saved', + }), + text: !keepPoliciesUpToDateSwitchValue + ? i18n.translate('xpack.fleet.integrations.keepPoliciesUpToDateEnabledSuccess', { + defaultMessage: + 'Fleet will automatically keep integration policies up to date for {title}', + values: { title }, + }) + : i18n.translate('xpack.fleet.integrations.keepPoliciesUpToDateDisabledSuccess', { + defaultMessage: + 'Fleet will not automatically keep integration policies up to date for {title}', + values: { title }, + }), + }); + } catch (error) { + notifications.toasts.addError(error, { + title: i18n.translate('xpack.fleet.integrations.integrationSavedError', { + defaultMessage: 'Error saving integration settings', + }), + toastMessage: i18n.translate('xpack.fleet.integrations.keepPoliciesUpToDateError', { + defaultMessage: 'Error saving integration settings for {title}', + values: { title }, + }), + }); + } + }; + + saveKeepPoliciesUpToDate(); + }, [ + keepPoliciesUpToDateSwitchValue, + notifications.toasts, + packageInfo.name, + packageInfo.version, + title, + ]); + const { status: installationStatus, version: installedVersion } = getPackageInstallStatus(name); const packageHasUsages = !!packagePoliciesData?.total; @@ -199,6 +269,16 @@ export const SettingsPage: React.FC<Props> = memo(({ packageInfo }: Props) => { </tr> </tbody> </table> + {shouldShowKeepPoliciesUpToDateSwitch && ( + <> + <KeepPoliciesUpToDateSwitch + checked={keepPoliciesUpToDateSwitchValue} + onChange={handleKeepPoliciesUpToDateSwitchChange} + /> + <EuiSpacer size="l" /> + </> + )} + {(updateAvailable || isUpgradingPackagePolicies) && ( <> <UpdatesAvailableMsg latestVersion={latestVersion} /> diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx index edb79b1f2c17b..62225d14d3857 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx @@ -89,7 +89,7 @@ function mapToCard( title: item.title, version: 'version' in item ? item.version || '' : '', release: 'release' in item ? item.release : undefined, - uiInternalPathUrl, + url: uiInternalPathUrl, }; } diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/missing_fleet_server_host_callout.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/missing_fleet_server_host_callout.tsx index 169ff86b40c88..c390b50c498fb 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/missing_fleet_server_host_callout.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/missing_fleet_server_host_callout.tsx @@ -30,7 +30,7 @@ export const MissingFleetServerHostCallout: React.FunctionComponent = () => { <EuiLink href={docLinks.links.fleet.guide} target="_blank" external> <FormattedMessage id="xpack.fleet.agentEnrollment.missingFleetHostGuideLink" - defaultMessage="Fleet User Guide" + defaultMessage="Fleet and Elastic Agent Guide" /> </EuiLink> ), diff --git a/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx b/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx index 9bedfca0d3bca..d10fd8336a37f 100644 --- a/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx +++ b/x-pack/plugins/fleet/public/components/settings_flyout/index.tsx @@ -382,7 +382,7 @@ export const SettingFlyout: React.FunctionComponent<Props> = ({ onClose }) => { > <FormattedMessage id="xpack.fleet.settings.userGuideLink" - defaultMessage="Fleet User Guide" + defaultMessage="Fleet and Elastic Agent Guide" /> </EuiLink> ), diff --git a/x-pack/plugins/fleet/public/constants/index.ts b/x-pack/plugins/fleet/public/constants/index.ts index a0e88bc58726a..32dd732c53dec 100644 --- a/x-pack/plugins/fleet/public/constants/index.ts +++ b/x-pack/plugins/fleet/public/constants/index.ts @@ -19,6 +19,9 @@ export { // Fleet Server index AGENTS_INDEX, ENROLLMENT_API_KEYS_INDEX, + // Preconfiguration + AUTO_UPDATE_PACKAGES, + DEFAULT_PACKAGES, } from '../../common/constants'; export * from './page_paths'; diff --git a/x-pack/plugins/fleet/public/hooks/use_merge_epr_with_replacements.ts b/x-pack/plugins/fleet/public/hooks/use_merge_epr_with_replacements.ts index a3c1fea5e744f..ac53badc2446d 100644 --- a/x-pack/plugins/fleet/public/hooks/use_merge_epr_with_replacements.ts +++ b/x-pack/plugins/fleet/public/hooks/use_merge_epr_with_replacements.ts @@ -10,6 +10,7 @@ import type { CustomIntegration, IntegrationCategory, } from '../../../../../src/plugins/custom_integrations/common'; +import { filterCustomIntegrations } from '../../../../../src/plugins/custom_integrations/public'; // Export this as a utility to find replacements for a package (e.g. in the overview-page for an EPR package) function findReplacementsForEprPackage( @@ -20,9 +21,7 @@ function findReplacementsForEprPackage( if (release === 'ga') { return []; } - return replacements.filter((customIntegration: CustomIntegration) => { - return customIntegration.eprOverlap === packageName; - }); + return filterCustomIntegrations(replacements, { eprPackageName: packageName }); } export function useMergeEprPackagesWithReplacements( diff --git a/x-pack/plugins/fleet/public/hooks/use_request/epm.ts b/x-pack/plugins/fleet/public/hooks/use_request/epm.ts index d6764aac7de00..a7078dd3a3f91 100644 --- a/x-pack/plugins/fleet/public/hooks/use_request/epm.ts +++ b/x-pack/plugins/fleet/public/hooks/use_request/epm.ts @@ -17,6 +17,8 @@ import type { GetInfoResponse, InstallPackageResponse, DeletePackageResponse, + UpdatePackageRequest, + UpdatePackageResponse, } from '../../types'; import type { GetStatsResponse } from '../../../common'; @@ -113,3 +115,11 @@ export const sendRemovePackage = (pkgkey: string) => { method: 'delete', }); }; + +export const sendUpdatePackage = (pkgkey: string, body: UpdatePackageRequest['body']) => { + return sendRequest<UpdatePackageResponse>({ + path: epmRouteService.getUpdatePath(pkgkey), + method: 'put', + body, + }); +}; diff --git a/x-pack/plugins/fleet/public/mock/plugin_dependencies.ts b/x-pack/plugins/fleet/public/mock/plugin_dependencies.ts index 5f3ee5c188b45..f78fe58a6ad88 100644 --- a/x-pack/plugins/fleet/public/mock/plugin_dependencies.ts +++ b/x-pack/plugins/fleet/public/mock/plugin_dependencies.ts @@ -26,5 +26,6 @@ export const createStartDepsMock = (): MockedFleetStartDeps => { return { data: dataPluginMock.createStartContract(), navigation: navigationPluginMock.createStartContract(), + customIntegrations: customIntegrationsMock.createStart(), }; }; diff --git a/x-pack/plugins/fleet/public/plugin.ts b/x-pack/plugins/fleet/public/plugin.ts index d7cc332910dc2..d23bfcfe7b888 100644 --- a/x-pack/plugins/fleet/public/plugin.ts +++ b/x-pack/plugins/fleet/public/plugin.ts @@ -16,8 +16,12 @@ import { i18n } from '@kbn/i18n'; import type { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; +import type { + CustomIntegrationsStart, + CustomIntegrationsSetup, +} from 'src/plugins/custom_integrations/public'; + import { DEFAULT_APP_CATEGORIES, AppNavLinkStatus } from '../../../../src/core/public'; -import type { CustomIntegrationsSetup } from '../../../../src/plugins/custom_integrations/public'; import type { DataPublicPluginSetup, @@ -76,6 +80,7 @@ export interface FleetSetupDeps { export interface FleetStartDeps { data: DataPublicPluginStart; navigation: NavigationPublicPluginStart; + customIntegrations: CustomIntegrationsStart; } export interface FleetStartServices extends CoreStart, FleetStartDeps { diff --git a/x-pack/plugins/fleet/public/types/index.ts b/x-pack/plugins/fleet/public/types/index.ts index 2328ca826da71..3ff0a760b5882 100644 --- a/x-pack/plugins/fleet/public/types/index.ts +++ b/x-pack/plugins/fleet/public/types/index.ts @@ -128,6 +128,8 @@ export { Installable, RegistryRelease, PackageSpecCategory, + UpdatePackageRequest, + UpdatePackageResponse, } from '../../common'; export * from './intra_app_route_state'; diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts index 2ce457242c6b5..bfb1f3ec433f2 100644 --- a/x-pack/plugins/fleet/server/constants/index.ts +++ b/x-pack/plugins/fleet/server/constants/index.ts @@ -58,6 +58,7 @@ export { // Preconfiguration PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE, PRECONFIGURATION_LATEST_KEYWORD, + AUTO_UPDATE_PACKAGES, } from '../../common'; export { diff --git a/x-pack/plugins/fleet/server/routes/epm/handlers.ts b/x-pack/plugins/fleet/server/routes/epm/handlers.ts index 16d583f8a8d1f..2324d1a423bfc 100644 --- a/x-pack/plugins/fleet/server/routes/epm/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/epm/handlers.ts @@ -22,6 +22,7 @@ import type { BulkInstallPackagesResponse, IBulkInstallPackageHTTPError, GetStatsResponse, + UpdatePackageResponse, } from '../../../common'; import type { GetCategoriesRequestSchema, @@ -33,6 +34,7 @@ import type { DeletePackageRequestSchema, BulkUpgradePackagesFromRegistryRequestSchema, GetStatsRequestSchema, + UpdatePackageRequestSchema, } from '../../types'; import { bulkInstallPackages, @@ -53,6 +55,7 @@ import { licenseService } from '../../services'; import { getArchiveEntry } from '../../services/epm/archive/cache'; import { getAsset } from '../../services/epm/archive/storage'; import { getPackageUsageStats } from '../../services/epm/packages/get'; +import { updatePackage } from '../../services/epm/packages/update'; export const getCategoriesHandler: RequestHandler< undefined, @@ -201,6 +204,28 @@ export const getInfoHandler: RequestHandler<TypeOf<typeof GetInfoRequestSchema.p } }; +export const updatePackageHandler: RequestHandler< + TypeOf<typeof UpdatePackageRequestSchema.params>, + unknown, + TypeOf<typeof UpdatePackageRequestSchema.body> +> = async (context, request, response) => { + try { + const { pkgkey } = request.params; + const savedObjectsClient = context.core.savedObjects.client; + + const { pkgName } = splitPkgKey(pkgkey); + + const res = await updatePackage({ savedObjectsClient, pkgName, ...request.body }); + const body: UpdatePackageResponse = { + response: res, + }; + + return response.ok({ body }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); + } +}; + export const getStatsHandler: RequestHandler<TypeOf<typeof GetStatsRequestSchema.params>> = async ( context, request, diff --git a/x-pack/plugins/fleet/server/routes/epm/index.ts b/x-pack/plugins/fleet/server/routes/epm/index.ts index 40316bd102e5f..684547dc1862c 100644 --- a/x-pack/plugins/fleet/server/routes/epm/index.ts +++ b/x-pack/plugins/fleet/server/routes/epm/index.ts @@ -18,6 +18,7 @@ import { DeletePackageRequestSchema, BulkUpgradePackagesFromRegistryRequestSchema, GetStatsRequestSchema, + UpdatePackageRequestSchema, } from '../../types'; import { @@ -31,6 +32,7 @@ import { deletePackageHandler, bulkInstallPackagesFromRegistryHandler, getStatsHandler, + updatePackageHandler, } from './handlers'; const MAX_FILE_SIZE_BYTES = 104857600; // 100MB @@ -90,6 +92,15 @@ export const registerRoutes = (router: IRouter) => { getInfoHandler ); + router.put( + { + path: EPM_API_ROUTES.INFO_PATTERN, + validate: UpdatePackageRequestSchema, + options: { tags: [`access:${PLUGIN_ID}-all`] }, + }, + updatePackageHandler + ); + router.post( { path: EPM_API_ROUTES.INSTALL_FROM_REGISTRY_PATTERN, diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index 83188e0047044..ac5ca401da000 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -44,6 +44,7 @@ import { } from './migrations/to_v7_13_0'; import { migratePackagePolicyToV7140, migrateInstallationToV7140 } from './migrations/to_v7_14_0'; import { migratePackagePolicyToV7150 } from './migrations/to_v7_15_0'; +import { migrateInstallationToV7160 } from './migrations/to_v7_16_0'; /* * Saved object types and mappings @@ -298,6 +299,7 @@ const getSavedObjectTypes = ( version: { type: 'keyword' }, internal: { type: 'boolean' }, removable: { type: 'boolean' }, + keep_policies_up_to_date: { type: 'boolean', index: false }, es_index_patterns: { enabled: false, type: 'object', @@ -332,6 +334,7 @@ const getSavedObjectTypes = ( migrations: { '7.14.0': migrateInstallationToV7140, '7.14.1': migrateInstallationToV7140, + '7.16.0': migrateInstallationToV7160, }, }, [ASSETS_SAVED_OBJECT_TYPE]: { diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_16_0.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_16_0.ts new file mode 100644 index 0000000000000..7d12c550ec406 --- /dev/null +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v7_16_0.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectMigrationFn } from 'kibana/server'; + +import type { Installation } from '../../../common'; +import { AUTO_UPDATE_PACKAGES, DEFAULT_PACKAGES } from '../../../common'; + +export const migrateInstallationToV7160: SavedObjectMigrationFn<Installation, Installation> = ( + installationDoc, + migrationContext +) => { + const updatedInstallationDoc = installationDoc; + + if ( + [...AUTO_UPDATE_PACKAGES, ...DEFAULT_PACKAGES].some( + (pkg) => pkg.name === updatedInstallationDoc.attributes.name + ) + ) { + updatedInstallationDoc.attributes.keep_policies_up_to_date = true; + } + + return updatedInstallationDoc; +}; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts index 1bbbb1bb9b6a2..9f66b5dd379ec 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts @@ -7,7 +7,12 @@ import type { ElasticsearchClient, SavedObject, SavedObjectsClientContract } from 'src/core/server'; -import { MAX_TIME_COMPLETE_INSTALL, ASSETS_SAVED_OBJECT_TYPE } from '../../../../common'; +import { + MAX_TIME_COMPLETE_INSTALL, + ASSETS_SAVED_OBJECT_TYPE, + PACKAGE_POLICY_SAVED_OBJECT_TYPE, + SO_SEARCH_LIMIT, +} from '../../../../common'; import type { InstallablePackage, InstallSource, PackageAssetReference } from '../../../../common'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; import type { AssetReference, Installation, InstallType } from '../../../types'; @@ -22,6 +27,8 @@ import { installIlmForDataStream } from '../elasticsearch/datastream_ilm/install import { saveArchiveEntries } from '../archive/storage'; import { ConcurrentInstallOperationError } from '../../../errors'; +import { packagePolicyService } from '../..'; + import { createInstallation, saveKibanaAssetsRefs, updateVersion } from './install'; import { deleteKibanaSavedObjectsAssets } from './remove'; @@ -192,11 +199,27 @@ export async function _installPackage({ // update to newly installed version when all assets are successfully installed if (installedPkg) await updateVersion(savedObjectsClient, pkgName, pkgVersion); - await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { - install_version: pkgVersion, - install_status: 'installed', - package_assets: packageAssetRefs, - }); + const updatedPackage = await savedObjectsClient.update<Installation>( + PACKAGES_SAVED_OBJECT_TYPE, + pkgName, + { + install_version: pkgVersion, + install_status: 'installed', + package_assets: packageAssetRefs, + } + ); + + // If the package is flagged with the `keep_policies_up_to_date` flag, upgrade its + // associated package policies after installation + if (updatedPackage.attributes.keep_policies_up_to_date) { + const policyIdsToUpgrade = await packagePolicyService.listIds(savedObjectsClient, { + page: 1, + perPage: SO_SEARCH_LIMIT, + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${pkgName}`, + }); + + await packagePolicyService.upgrade(savedObjectsClient, esClient, policyIdsToUpgrade.items); + } return [ ...installedKibanaAssetsRefs, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts index 0e23981b95fcd..d4f988e5fba8c 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts @@ -137,6 +137,7 @@ export async function getPackageInfo(options: { assets: Registry.groupPathsByService(paths || []), removable: !isUnremovablePackage(pkgName), notice: Registry.getNoticePath(paths || []), + keepPoliciesUpToDate: savedObject?.attributes.keep_policies_up_to_date ?? false, }; const updated = { ...packageInfo, ...additions }; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get_install_type.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/get_install_type.test.ts index 155cd67e60287..6bc962165f1d2 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get_install_type.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get_install_type.test.ts @@ -28,6 +28,7 @@ const mockInstallation: SavedObject<Installation> = { install_version: '1.0.0', install_started_at: new Date().toISOString(), install_source: 'registry', + keep_policies_up_to_date: false, }, }; const mockInstallationUpdateFail: SavedObject<Installation> = { @@ -46,6 +47,7 @@ const mockInstallationUpdateFail: SavedObject<Installation> = { install_version: '1.0.1', install_started_at: new Date().toISOString(), install_source: 'registry', + keep_policies_up_to_date: false, }, }; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index bd1968f03c263..e71ef5e002884 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -457,6 +457,7 @@ export async function createInstallation(options: { install_status: 'installing', install_started_at: new Date().toISOString(), install_source: installSource, + keep_policies_up_to_date: false, }, { id: pkgName, overwrite: true } ); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/update.ts b/x-pack/plugins/fleet/server/services/epm/packages/update.ts new file mode 100644 index 0000000000000..84c756983fa07 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/packages/update.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectsClientContract } from 'kibana/server'; +import type { TypeOf } from '@kbn/config-schema'; + +import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; +import type { Installation, UpdatePackageRequestSchema } from '../../../types'; +import { IngestManagerError } from '../../../errors'; + +import { getInstallationObject, getPackageInfo } from './get'; + +export async function updatePackage( + options: { + savedObjectsClient: SavedObjectsClientContract; + pkgName: string; + keepPoliciesUpToDate?: boolean; + } & TypeOf<typeof UpdatePackageRequestSchema.body> +) { + const { savedObjectsClient, pkgName, keepPoliciesUpToDate } = options; + const installedPackage = await getInstallationObject({ savedObjectsClient, pkgName }); + + if (!installedPackage) { + throw new IngestManagerError(`package ${pkgName} is not installed`); + } + + await savedObjectsClient.update<Installation>(PACKAGES_SAVED_OBJECT_TYPE, installedPackage.id, { + keep_policies_up_to_date: keepPoliciesUpToDate ?? false, + }); + + const packageInfo = await getPackageInfo({ + savedObjectsClient, + pkgName, + pkgVersion: installedPackage.attributes.version, + }); + + return packageInfo; +} diff --git a/x-pack/plugins/fleet/server/services/managed_package_policies.test.ts b/x-pack/plugins/fleet/server/services/managed_package_policies.test.ts new file mode 100644 index 0000000000000..a53b1fe648905 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/managed_package_policies.test.ts @@ -0,0 +1,106 @@ +/* + * 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 { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks'; + +import { upgradeManagedPackagePolicies } from './managed_package_policies'; +import { packagePolicyService } from './package_policy'; +import { getPackageInfo } from './epm/packages'; + +jest.mock('./package_policy'); +jest.mock('./epm/packages'); +jest.mock('./app_context', () => { + return { + ...jest.requireActual('./app_context'), + appContextService: { + getLogger: jest.fn(() => { + return { debug: jest.fn() }; + }), + }, + }; +}); + +describe('managed package policies', () => { + afterEach(() => { + (packagePolicyService.get as jest.Mock).mockReset(); + (getPackageInfo as jest.Mock).mockReset(); + }); + + it('should not upgrade policies for non-managed package', async () => { + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + const soClient = savedObjectsClientMock.create(); + + (packagePolicyService.get as jest.Mock).mockImplementationOnce( + (savedObjectsClient: any, id: string) => { + return { + id, + inputs: {}, + version: '', + revision: 1, + updated_at: '', + updated_by: '', + created_at: '', + created_by: '', + package: { + name: 'non-managed-package', + title: 'Non-Managed Package', + version: '0.0.1', + }, + }; + } + ); + + (getPackageInfo as jest.Mock).mockImplementationOnce( + ({ savedObjectsClient, pkgName, pkgVersion }) => ({ + name: pkgName, + version: pkgVersion, + keepPoliciesUpToDate: false, + }) + ); + + await upgradeManagedPackagePolicies(soClient, esClient, ['non-managed-package-id']); + + expect(packagePolicyService.upgrade).not.toBeCalled(); + }); + + it('should upgrade policies for managed package', async () => { + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + const soClient = savedObjectsClientMock.create(); + + (packagePolicyService.get as jest.Mock).mockImplementationOnce( + (savedObjectsClient: any, id: string) => { + return { + id, + inputs: {}, + version: '', + revision: 1, + updated_at: '', + updated_by: '', + created_at: '', + created_by: '', + package: { + name: 'managed-package', + title: 'Managed Package', + version: '0.0.1', + }, + }; + } + ); + + (getPackageInfo as jest.Mock).mockImplementationOnce( + ({ savedObjectsClient, pkgName, pkgVersion }) => ({ + name: pkgName, + version: pkgVersion, + keepPoliciesUpToDate: true, + }) + ); + + await upgradeManagedPackagePolicies(soClient, esClient, ['managed-package-id']); + + expect(packagePolicyService.upgrade).toBeCalledWith(soClient, esClient, ['managed-package-id']); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/managed_package_policies.ts b/x-pack/plugins/fleet/server/services/managed_package_policies.ts new file mode 100644 index 0000000000000..73f85525f4c60 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/managed_package_policies.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; + +import { AUTO_UPDATE_PACKAGES } from '../../common'; + +import { appContextService } from './app_context'; +import { getPackageInfo } from './epm/packages'; +import { packagePolicyService } from './package_policy'; + +/** + * Upgrade any package policies for packages installed through setup that are denoted as `AUTO_UPGRADE` packages + * or have the `keep_policies_up_to_date` flag set to `true` + */ +export const upgradeManagedPackagePolicies = async ( + soClient: SavedObjectsClientContract, + esClient: ElasticsearchClient, + packagePolicyIds: string[] +) => { + const policyIdsToUpgrade: string[] = []; + + for (const packagePolicyId of packagePolicyIds) { + const packagePolicy = await packagePolicyService.get(soClient, packagePolicyId); + + if (!packagePolicy || !packagePolicy.package) { + continue; + } + + const packageInfo = await getPackageInfo({ + savedObjectsClient: soClient, + pkgName: packagePolicy.package.name, + pkgVersion: packagePolicy.package.version, + }); + + const shouldUpgradePolicies = + AUTO_UPDATE_PACKAGES.some((pkg) => pkg.name === packageInfo.name) || + packageInfo.keepPoliciesUpToDate; + + if (shouldUpgradePolicies) { + policyIdsToUpgrade.push(packagePolicy.id); + } + } + + if (policyIdsToUpgrade.length) { + appContextService + .getLogger() + .debug( + `Upgrading ${policyIdsToUpgrade.length} package policies: ${policyIdsToUpgrade.join(', ')}` + ); + + await packagePolicyService.upgrade(soClient, esClient, policyIdsToUpgrade); + } +}; diff --git a/x-pack/plugins/fleet/server/services/package_policy.test.ts b/x-pack/plugins/fleet/server/services/package_policy.test.ts index 31f1440135436..c04134e97b415 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.test.ts @@ -1085,7 +1085,98 @@ describe('Package policy service', () => { }); describe('overridePackageInputs', () => { - it('should override variable in base package policy', () => { + describe('when variable is already defined', () => { + it('preserves original variable value without overwriting', () => { + const basePackagePolicy: NewPackagePolicy = { + name: 'base-package-policy', + description: 'Base Package Policy', + namespace: 'default', + enabled: true, + policy_id: 'xxxx', + output_id: 'xxxx', + package: { + name: 'test-package', + title: 'Test Package', + version: '0.0.1', + }, + inputs: [ + { + type: 'logs', + policy_template: 'template_1', + enabled: true, + vars: { + path: { + type: 'text', + value: ['/var/log/logfile.log'], + }, + }, + streams: [], + }, + ], + }; + + const packageInfo: PackageInfo = { + name: 'test-package', + description: 'Test Package', + title: 'Test Package', + version: '0.0.1', + latestVersion: '0.0.1', + release: 'experimental', + format_version: '1.0.0', + owner: { github: 'elastic/fleet' }, + policy_templates: [ + { + name: 'template_1', + title: 'Template 1', + description: 'Template 1', + inputs: [ + { + type: 'logs', + title: 'Log', + description: 'Log Input', + vars: [ + { + name: 'path', + type: 'text', + }, + ], + }, + ], + }, + ], + // @ts-ignore + assets: {}, + }; + + const inputsOverride: NewPackagePolicyInput[] = [ + { + type: 'logs', + enabled: true, + streams: [], + vars: { + path: { + type: 'text', + value: '/var/log/new-logfile.log', + }, + }, + }, + ]; + + const result = overridePackageInputs( + basePackagePolicy, + packageInfo, + // TODO: Update this type assertion when the `InputsOverride` type is updated such + // that it no longer causes unresolvable type errors when used directly + inputsOverride as InputsOverride[], + false + ); + expect(result.inputs[0]?.vars?.path.value).toEqual(['/var/log/logfile.log']); + }); + }); + }); + + describe('when variable is undefined in original object', () => { + it('adds the variable definition to the resulting object', () => { const basePackagePolicy: NewPackagePolicy = { name: 'base-package-policy', description: 'Base Package Policy', @@ -1138,6 +1229,10 @@ describe('Package policy service', () => { name: 'path', type: 'text', }, + { + name: 'path_2', + type: 'text', + }, ], }, ], @@ -1157,6 +1252,10 @@ describe('Package policy service', () => { type: 'text', value: '/var/log/new-logfile.log', }, + path_2: { + type: 'text', + value: '/var/log/custom.log', + }, }, }, ]; @@ -1169,7 +1268,7 @@ describe('Package policy service', () => { inputsOverride as InputsOverride[], false ); - expect(result.inputs[0]?.vars?.path.value).toBe('/var/log/new-logfile.log'); + expect(result.inputs[0]?.vars?.path_2.value).toEqual('/var/log/custom.log'); }); }); }); diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 9b02d6eaff495..93f04a55d233b 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -980,7 +980,7 @@ export function overridePackageInputs( ({ name }) => name === input.policy_template ); - // Ignore any policy template removes in the new package version + // Ignore any policy templates removed in the new package version if (!policyTemplate) { return false; } @@ -1000,7 +1000,7 @@ export function overridePackageInputs( // If there's no corresponding input on the original package policy, just // take the override value from the new package as-is. This case typically - // occurs when inputs or package policies are added/removed between versions. + // occurs when inputs or package policy templates are added/removed between versions. if (originalInput === undefined) { inputs.push(override as NewPackagePolicyInput); continue; @@ -1092,7 +1092,14 @@ function deepMergeVars(original: any, override: any): any { for (const { name, ...overrideVal } of overrideVars) { const originalVar = original.vars[name]; + result.vars[name] = { ...originalVar, ...overrideVal }; + + // Ensure that any value from the original object is persisted on the newly merged resulting object, + // even if we merge other data about the given variable + if (originalVar?.value) { + result.vars[name].value = originalVar.value; + } } return result; diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts index 43887bc2787f4..d0ae995358632 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts @@ -137,6 +137,7 @@ jest.mock('./package_policy', () => ({ ...jest.requireActual('./package_policy'), packagePolicyService: { getByIDs: jest.fn().mockReturnValue([]), + listIds: jest.fn().mockReturnValue({ items: [] }), create(soClient: any, esClient: any, newPackagePolicy: NewPackagePolicy) { return { id: 'mocked', @@ -144,6 +145,12 @@ jest.mock('./package_policy', () => ({ ...newPackagePolicy, }; }, + get(soClient: any, id: string) { + return { + id: 'mocked', + version: 'mocked', + }; + }, }, })); diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts index 30c5c27c68916..a444f8bdaa4da 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts @@ -35,6 +35,7 @@ import { agentPolicyService, addPackageToAgentPolicy } from './agent_policy'; import type { InputsOverride } from './package_policy'; import { overridePackageInputs } from './package_policy'; import { appContextService } from './app_context'; +import { upgradeManagedPackagePolicies } from './managed_package_policies'; import { outputService } from './output'; interface PreconfigurationResult { @@ -313,6 +314,17 @@ export async function ensurePreconfiguredPackagesAndPolicies( } } + try { + const fulfilledPolicyPackagePolicyIds = fulfilledPolicies.flatMap<string>( + ({ policy }) => policy?.package_policies as string[] + ); + + await upgradeManagedPackagePolicies(soClient, esClient, fulfilledPolicyPackagePolicyIds); + // Swallow errors that occur when upgrading + } catch (error) { + appContextService.getLogger().error(error); + } + return { policies: fulfilledPolicies.map((p) => p.policy diff --git a/x-pack/plugins/fleet/server/types/rest_spec/epm.ts b/x-pack/plugins/fleet/server/types/rest_spec/epm.ts index 25f1e766a7476..918def62a9d0e 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/epm.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/epm.ts @@ -35,6 +35,15 @@ export const GetInfoRequestSchema = { }), }; +export const UpdatePackageRequestSchema = { + params: schema.object({ + pkgkey: schema.string(), + }), + body: schema.object({ + keepPoliciesUpToDate: schema.boolean(), + }), +}; + export const GetStatsRequestSchema = { params: schema.object({ pkgName: schema.string(), diff --git a/x-pack/plugins/fleet/storybook/context/fixtures/packages.ts b/x-pack/plugins/fleet/storybook/context/fixtures/packages.ts index 251024a4e7cdb..2a6012272d4b8 100644 --- a/x-pack/plugins/fleet/storybook/context/fixtures/packages.ts +++ b/x-pack/plugins/fleet/storybook/context/fixtures/packages.ts @@ -985,6 +985,7 @@ export const response: GetPackagesResponse['response'] = [ install_status: 'installed', install_started_at: '2021-08-25T19:44:41.090Z', install_source: 'registry', + keep_policies_up_to_date: false, }, references: [], coreMigrationVersion: '7.14.0', @@ -1113,6 +1114,7 @@ export const response: GetPackagesResponse['response'] = [ install_status: 'installed', install_started_at: '2021-08-25T19:44:37.078Z', install_source: 'registry', + keep_policies_up_to_date: false, }, references: [], coreMigrationVersion: '7.14.0', @@ -4268,6 +4270,7 @@ export const response: GetPackagesResponse['response'] = [ install_status: 'installed', install_started_at: '2021-08-25T19:44:43.380Z', install_source: 'registry', + keep_policies_up_to_date: false, }, references: [], coreMigrationVersion: '7.14.0', diff --git a/x-pack/plugins/fleet/storybook/context/index.tsx b/x-pack/plugins/fleet/storybook/context/index.tsx index 6c6b2e479d234..e5a360c28385b 100644 --- a/x-pack/plugins/fleet/storybook/context/index.tsx +++ b/x-pack/plugins/fleet/storybook/context/index.tsx @@ -13,12 +13,12 @@ import { createBrowserHistory } from 'history'; import { I18nProvider } from '@kbn/i18n/react'; import { ScopedHistory } from '../../../../../src/core/public'; +import { getStorybookContextProvider } from '../../../../../src/plugins/custom_integrations/storybook'; import { IntegrationsAppContext } from '../../public/applications/integrations/app'; import type { FleetConfigType, FleetStartServices } from '../../public/plugin'; -// TODO: This is a contract leak, and should be on the context, rather than a setter. +// TODO: These are contract leaks, and should be on the context, rather than a setter. import { setHttpClient } from '../../public/hooks/use_request'; - import { setCustomIntegrations } from '../../public/services/custom_integrations'; import { getApplication } from './application'; @@ -36,7 +36,6 @@ import { stubbedStartServices } from './stubs'; // Expect this to grow as components that are given Stories need access to mocked services. export const StorybookContext: React.FC<{ storyContext?: StoryContext }> = ({ children: storyChildren, - storyContext, }) => { const basepath = ''; const browserHistory = createBrowserHistory(); @@ -56,6 +55,9 @@ export const StorybookContext: React.FC<{ storyContext?: StoryContext }> = ({ injectedMetadata: { getInjectedVar: () => null, }, + customIntegrations: { + ContextProvider: getStorybookContextProvider(), + }, ...stubbedStartServices, }; diff --git a/x-pack/plugins/fleet/storybook/decorator.tsx b/x-pack/plugins/fleet/storybook/decorator.tsx index 91d6cc41e6b9a..8e68249809574 100644 --- a/x-pack/plugins/fleet/storybook/decorator.tsx +++ b/x-pack/plugins/fleet/storybook/decorator.tsx @@ -10,6 +10,6 @@ import type { DecoratorFn } from '@storybook/react'; import { StorybookContext } from './context'; -export const decorator: DecoratorFn = (story: Function) => { +export const decorator: DecoratorFn = (story, storybook) => { return <StorybookContext>{story()}</StorybookContext>; }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts index 1c75ef314b728..b24defcdcd79c 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/test_subjects.ts @@ -25,6 +25,7 @@ export type TestSubjects = | 'ilmPolicyLink' | 'includeStatsSwitch' | 'includeManagedSwitch' + | 'indexManagementHeaderContent' | 'indexTable' | 'indexTableIncludeHiddenIndicesToggle' | 'indexTableIndexNameLink' diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/home.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/home.helpers.ts index 921812943a3e6..a15e4f2a613d3 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/home.helpers.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/home.helpers.ts @@ -24,24 +24,31 @@ const initTestBed = registerTestBed(WithAppDependencies(IndexManagementHome), te export interface HomeTestBed extends TestBed<TestSubjects> { actions: { selectHomeTab: (tab: 'indicesTab' | 'templatesTab') => void; + toggleHiddenIndices: () => void; }; } export const setup = async (): Promise<HomeTestBed> => { const testBed = await initTestBed(); + const { find } = testBed; /** * User Actions */ const selectHomeTab = (tab: 'indicesTab' | 'templatesTab') => { - testBed.find(tab).simulate('click'); + find(tab).simulate('click'); + }; + + const toggleHiddenIndices = async function () { + find('indexTableIncludeHiddenIndicesToggle').simulate('click'); }; return { ...testBed, actions: { selectHomeTab, + toggleHiddenIndices, }, }; }; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/home.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/home.test.ts index 42863d36050d7..426bb11f3c733 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/home.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/home.test.ts @@ -54,15 +54,18 @@ describe('<IndexManagementHome />', () => { }); describe('tabs', () => { - test('should have 2 tabs', () => { + test('should have 4 tabs', () => { const { find } = testBed; - const templatesTab = find('templatesTab'); - const indicesTab = find('indicesTab'); - expect(indicesTab.length).toBe(1); - expect(indicesTab.text()).toEqual('Indices'); - expect(templatesTab.length).toBe(1); - expect(templatesTab.text()).toEqual('Index Templates'); + const indexManagementContainer = find('indexManagementHeaderContent'); + const tabListContainer = indexManagementContainer.find('.euiTabs'); + const allTabs = tabListContainer.children(); + const allTabsLabels = ['Indices', 'Data Streams', 'Index Templates', 'Component Templates']; + + expect(allTabs.length).toBe(4); + for (let i = 0; i < allTabs.length; i++) { + expect(tabListContainer.childAt(i).text()).toEqual(allTabsLabels[i]); + } }); test('should navigate to Index Templates tab', async () => { diff --git a/x-pack/plugins/index_management/public/application/components/no_match/no_match.tsx b/x-pack/plugins/index_management/public/application/components/no_match/no_match.tsx index 34cbc10a32d09..8546a179fafdf 100644 --- a/x-pack/plugins/index_management/public/application/components/no_match/no_match.tsx +++ b/x-pack/plugins/index_management/public/application/components/no_match/no_match.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; export const NoMatch = () => ( - <div> + <div data-test-subj="noIndicesMessage"> <FormattedMessage id="xpack.idxMgmt.noMatch.noIndicesDescription" defaultMessage="No indices to show" diff --git a/x-pack/plugins/index_management/public/application/sections/home/home.tsx b/x-pack/plugins/index_management/public/application/sections/home/home.tsx index 003aa045f9591..8fce2d354f87b 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/home.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/home.tsx @@ -85,6 +85,7 @@ export const IndexManagementHome: React.FunctionComponent<RouteComponentProps<Ma return ( <> <EuiPageHeader + data-test-subj="indexManagementHeaderContent" pageTitle={ <span data-test-subj="appTitle"> <FormattedMessage id="xpack.idxMgmt.home.appTitle" defaultMessage="Index Management" /> diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index bbbb0e917d518..72d9ea9e39def 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -7,7 +7,7 @@ import { first, get, last } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { ALERT_REASON } from '@kbn/rule-data-utils'; +import { ALERT_REASON, ALERT_RULE_PARAMS } from '@kbn/rule-data-utils'; import moment from 'moment'; import { getCustomMetricLabel } from '../../../../common/formatters/get_custom_metric_label'; import { toMetricOpt } from '../../../../common/snapshot_metric_i18n'; @@ -79,6 +79,7 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = id, fields: { [ALERT_REASON]: reason, + [ALERT_RULE_PARAMS]: JSON.stringify(params), }, }); diff --git a/x-pack/plugins/ingest_pipelines/README.md b/x-pack/plugins/ingest_pipelines/README.md index 00d4f5a91863d..dd7c130c7a72d 100644 --- a/x-pack/plugins/ingest_pipelines/README.md +++ b/x-pack/plugins/ingest_pipelines/README.md @@ -1,9 +1,9 @@ -# Ingest Node Pipelines UI +# Ingest Pipelines UI ## Summary -The `ingest_pipelines` plugin provides Kibana support for [Elasticsearch's ingest nodes](https://www.elastic.co/guide/en/elasticsearch/reference/master/ingest.html). Please refer to the Elasticsearch documentation for more details. +The `ingest_pipelines` plugin provides Kibana support for [Elasticsearch's ingest pipelines](https://www.elastic.co/guide/en/elasticsearch/reference/master/ingest.html). -This plugin allows Kibana to create, edit, clone and delete ingest node pipelines. It also provides support to simulate a pipeline. +This plugin allows Kibana to create, edit, clone and delete ingest pipelines. It also provides support to simulate a pipeline. It requires a Basic license and the following cluster privileges: `manage_pipeline` and `cluster:monitor/nodes/info`. @@ -11,7 +11,7 @@ It requires a Basic license and the following cluster privileges: `manage_pipeli ## Development -A new app called Ingest Node Pipelines is registered in the Management section and follows a typical CRUD UI pattern. The client-side portion of this app lives in [public/application](public/application) and uses endpoints registered in [server/routes/api](server/routes/api). For more information on the pipeline processors editor component, check out the [component readme](public/application/components/pipeline_processors_editor/README.md). +A new app called Ingest Pipelines is registered in the Management section and follows a typical CRUD UI pattern. The client-side portion of this app lives in [public/application](public/application) and uses endpoints registered in [server/routes/api](server/routes/api). For more information on the pipeline processors editor component, check out the [component readme](public/application/components/pipeline_processors_editor/README.md). See the [kibana contributing guide](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md) for instructions on setting up your development environment. @@ -25,7 +25,7 @@ The app has the following test coverage: ### Quick steps for manual testing -You can run the following request in Console to create an ingest node pipeline: +You can run the following request in Console to create an ingest pipeline: ``` PUT _ingest/pipeline/test_pipeline @@ -73,7 +73,7 @@ PUT _ingest/pipeline/test_pipeline } ``` -Then, go to the Ingest Node Pipelines UI to edit, delete, clone, or view details of the pipeline. +Then, go to the Ingest Pipelines UI to edit, delete, clone, or view details of the pipeline. To simulate a pipeline, go to the "Edit" page of your pipeline. Click the "Add documents" link under the "Processors" section. You may add the following sample documents to test the pipeline: diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts index 78e3f2dab0d1d..19a2abb5a5a52 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_list.test.ts @@ -52,11 +52,11 @@ describe('<PipelinesList />', () => { // Verify app title expect(exists('appTitle')).toBe(true); - expect(find('appTitle').text()).toEqual('Ingest Node Pipelines'); + expect(find('appTitle').text()).toEqual('Ingest Pipelines'); // Verify documentation link expect(exists('documentationLink')).toBe(true); - expect(find('documentationLink').text()).toBe('Ingest Node Pipelines docs'); + expect(find('documentationLink').text()).toBe('Ingest Pipelines docs'); // Verify create button exists expect(exists('createPipelineButton')).toBe(true); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx index 0d61d839156e2..eae48a6b46dd1 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/shared/map_processor_type_to_form.tsx @@ -557,7 +557,7 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { defaultMessage: 'Pipeline', }), typeDescription: i18n.translate('xpack.ingestPipelines.processors.description.pipeline', { - defaultMessage: 'Runs another ingest node pipeline.', + defaultMessage: 'Runs another ingest pipeline.', }), getDefaultDescription: ({ name }) => i18n.translate('xpack.ingestPipelines.processors.defaultDescription.pipeline', { diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx index ae68cfcb399f0..95621601011f9 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx @@ -153,7 +153,7 @@ export const PipelinesList: React.FunctionComponent<RouteComponentProps> = ({ <span data-test-subj="appTitle"> <FormattedMessage id="xpack.ingestPipelines.list.listTitle" - defaultMessage="Ingest Node Pipelines" + defaultMessage="Ingest Pipelines" /> </span> } @@ -172,7 +172,7 @@ export const PipelinesList: React.FunctionComponent<RouteComponentProps> = ({ > <FormattedMessage id="xpack.ingestPipelines.list.pipelinesDocsLinkText" - defaultMessage="Ingest Node Pipelines docs" + defaultMessage="Ingest Pipelines docs" /> </EuiButtonEmpty>, ]} diff --git a/x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts b/x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts index 138fdf4e8ead6..f1b2d22e776b9 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/services/breadcrumbs.ts @@ -11,7 +11,7 @@ import { ManagementAppMountParams } from '../../../../../../src/plugins/manageme type SetBreadcrumbs = ManagementAppMountParams['setBreadcrumbs']; const homeBreadcrumbText = i18n.translate('xpack.ingestPipelines.breadcrumb.pipelinesLabel', { - defaultMessage: 'Ingest Node Pipelines', + defaultMessage: 'Ingest Pipelines', }); export class BreadcrumbService { diff --git a/x-pack/plugins/ingest_pipelines/public/plugin.ts b/x-pack/plugins/ingest_pipelines/public/plugin.ts index 4e85490de6209..68dc2c1801e0c 100644 --- a/x-pack/plugins/ingest_pipelines/public/plugin.ts +++ b/x-pack/plugins/ingest_pipelines/public/plugin.ts @@ -25,7 +25,7 @@ export class IngestPipelinesPlugin apiService.setup(http, uiMetricService); const pluginName = i18n.translate('xpack.ingestPipelines.appTitle', { - defaultMessage: 'Ingest Node Pipelines', + defaultMessage: 'Ingest Pipelines', }); management.sections.section.ingest.registerApp({ diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index 263198871f07a..2e0ab2401c70f 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -7,6 +7,7 @@ import { isEqual, uniqBy } from 'lodash'; import React from 'react'; +import { i18n } from '@kbn/i18n'; import { render, unmountComponentAtNode } from 'react-dom'; import type { ExecutionContextSearch, @@ -41,11 +42,7 @@ import { ReferenceOrValueEmbeddable, } from '../../../../../src/plugins/embeddable/public'; import { Document, injectFilterReferences } from '../persistence'; -import { - ExpressionWrapper, - ExpressionWrapperProps, - savedObjectConflictError, -} from './expression_wrapper'; +import { ExpressionWrapper, ExpressionWrapperProps } from './expression_wrapper'; import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; import { isLensBrushEvent, @@ -63,6 +60,7 @@ import { LensAttributeService } from '../lens_attribute_service'; import type { ErrorMessage } from '../editor_frame_service/types'; import { getLensInspectorService, LensInspector } from '../lens_inspector_service'; import { SharingSavedObjectProps } from '../types'; +import type { SpacesPluginStart } from '../../../spaces/public'; export type LensSavedObjectAttributes = Omit<Document, 'savedObjectId' | 'type'>; export interface ResolvedLensSavedObjectAttributes extends LensSavedObjectAttributes { @@ -108,6 +106,7 @@ export interface LensEmbeddableDeps { getTriggerCompatibleActions?: UiActionsStart['getTriggerCompatibleActions']; capabilities: { canSaveVisualizations: boolean; canSaveDashboards: boolean }; usageCollection?: UsageCollectionSetup; + spaces?: SpacesPluginStart; } export class Embeddable @@ -281,8 +280,17 @@ export class Embeddable }; const { ast, errors } = await this.deps.documentToExpression(this.savedVis); this.errors = errors; - if (sharingSavedObjectProps?.outcome === 'conflict') { - const conflictError = savedObjectConflictError(sharingSavedObjectProps.errorJSON!); + if (sharingSavedObjectProps?.outcome === 'conflict' && this.deps.spaces) { + const conflictError = { + shortMessage: i18n.translate('xpack.lens.embeddable.legacyURLConflict.shortMessage', { + defaultMessage: `You've encountered a URL conflict`, + }), + longMessage: ( + <this.deps.spaces.ui.components.getSavedObjectConflictMessage + json={sharingSavedObjectProps.errorJSON!} + /> + ), + }; this.errors = this.errors ? [...this.errors, conflictError] : [conflictError]; } this.expression = ast ? toExpression(ast) : null; diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts index 954905c51a4b7..e51ec4c3e5588 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts +++ b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts @@ -24,6 +24,7 @@ import { LensAttributeService } from '../lens_attribute_service'; import { DOC_TYPE } from '../../common/constants'; import { ErrorMessage } from '../editor_frame_service/types'; import { extract, inject } from '../../common/embeddable_factory'; +import type { SpacesPluginStart } from '../../../spaces/public'; export interface LensEmbeddableStartServices { timefilter: TimefilterContract; @@ -38,6 +39,7 @@ export interface LensEmbeddableStartServices { documentToExpression: ( doc: Document ) => Promise<{ ast: Ast | null; errors: ErrorMessage[] | undefined }>; + spaces?: SpacesPluginStart; } export class EmbeddableFactory implements EmbeddableFactoryDefinition { @@ -90,6 +92,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { capabilities, usageCollection, inspector, + spaces, } = await this.getStartServices(); const { Embeddable } = await import('../async_services'); @@ -110,6 +113,7 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { canSaveVisualizations: Boolean(capabilities.visualize.save), }, usageCollection, + spaces, }, input, parent diff --git a/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx b/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx index c827fe74cc52b..3de914d13d69d 100644 --- a/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx +++ b/x-pack/plugins/lens/public/embeddable/expression_wrapper.tsx @@ -5,20 +5,10 @@ * 2.0. */ -import React, { useState } from 'react'; +import React from 'react'; import { I18nProvider } from '@kbn/i18n/react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiText, - EuiIcon, - EuiEmptyPrompt, - EuiButtonEmpty, - EuiCallOut, - EuiSpacer, - EuiLink, -} from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiText, EuiIcon, EuiEmptyPrompt } from '@elastic/eui'; import { ExpressionRendererEvent, ReactExpressionRendererType, @@ -28,7 +18,6 @@ import type { KibanaExecutionContext } from 'src/core/public'; import { ExecutionContextSearch } from 'src/plugins/data/public'; import { DefaultInspectorAdapters, RenderMode } from 'src/plugins/expressions'; import classNames from 'classnames'; -import { i18n } from '@kbn/i18n'; import { getOriginalRequestErrorMessages } from '../editor_frame_service/error_helper'; import { ErrorMessage } from '../editor_frame_service/types'; import { LensInspector } from '../lens_inspector_service'; @@ -172,52 +161,3 @@ export function ExpressionWrapper({ </I18nProvider> ); } - -const SavedObjectConflictMessage = ({ json }: { json: string }) => { - const [expandError, setExpandError] = useState(false); - return ( - <> - <FormattedMessage - id="xpack.lens.embeddable.legacyURLConflict.longMessage" - defaultMessage="Disable the {documentationLink} associated with this object." - values={{ - documentationLink: ( - <EuiLink - external - href="https://www.elastic.co/guide/en/kibana/master/legacy-url-aliases.html" - target="_blank" - > - {i18n.translate('xpack.lens.embeddable.legacyURLConflict.documentationLinkText', { - defaultMessage: 'legacy URL alias', - })} - </EuiLink> - ), - }} - /> - <EuiSpacer /> - {expandError ? ( - <EuiCallOut - title={i18n.translate('xpack.lens.embeddable.legacyURLConflict.expandErrorText', { - defaultMessage: `This object has the same URL as a legacy alias. Disable the alias to resolve this error : {json}`, - values: { json }, - })} - color="danger" - iconType="alert" - /> - ) : ( - <EuiButtonEmpty onClick={() => setExpandError(true)}> - {i18n.translate('xpack.lens.embeddable.legacyURLConflict.expandError', { - defaultMessage: `Show more`, - })} - </EuiButtonEmpty> - )} - </> - ); -}; - -export const savedObjectConflictError = (json: string): ErrorMessage => ({ - shortMessage: i18n.translate('xpack.lens.embeddable.legacyURLConflict.shortMessage', { - defaultMessage: `You've encountered a URL conflict`, - }), - longMessage: <SavedObjectConflictMessage json={json} />, -}); diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index 7891b5990989c..1532b2b099104 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -212,6 +212,7 @@ export class LensPlugin { uiActions: plugins.uiActions, usageCollection, inspector: plugins.inspector, + spaces: plugins.spaces, }; }; diff --git a/x-pack/plugins/licensing/README.md b/x-pack/plugins/licensing/README.md index 3de1fe9cae425..52204c66dd2bf 100644 --- a/x-pack/plugins/licensing/README.md +++ b/x-pack/plugins/licensing/README.md @@ -126,6 +126,7 @@ This change makes NP & LP licensing service not compatible. We have to keep both **LP**: `xpack.xpack_main.xpack_api_polling_frequency_millis` **NP**: `xpack.licensing.api_polling_frequency` +Support for deprecated `xpack.xpack_main.xpack_api_polling_frequency_millis` is removed in v8.0.0. See https://github.com/elastic/kibana/issues/103915 for more details. #### License **NP**: `mode` field is provided, but deprecated. diff --git a/x-pack/plugins/licensing/server/licensing_config.ts b/x-pack/plugins/licensing/server/licensing_config.ts index a27eaba56df50..85de9f84a703f 100644 --- a/x-pack/plugins/licensing/server/licensing_config.ts +++ b/x-pack/plugins/licensing/server/licensing_config.ts @@ -18,10 +18,4 @@ export const config: PluginConfigDescriptor<LicenseConfigType> = { schema: schema.object({ api_polling_frequency: schema.duration({ defaultValue: '30s' }), }), - deprecations: ({ renameFromRoot }) => [ - renameFromRoot( - 'xpack.xpack_main.xpack_api_polling_frequency_millis', - 'xpack.licensing.api_polling_frequency' - ), - ], }; diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json index bfd501dbcb295..2aa2e4a756490 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -29,6 +29,7 @@ "savedObjectsTagging", "charts", "security", + "spaces", "usageCollection" ], "ui": true, diff --git a/x-pack/plugins/maps/public/embeddable/_index.scss b/x-pack/plugins/maps/public/embeddable/_index.scss index 966236f54d259..07c874d966fec 100644 --- a/x-pack/plugins/maps/public/embeddable/_index.scss +++ b/x-pack/plugins/maps/public/embeddable/_index.scss @@ -5,4 +5,12 @@ flex: 1 1 100%; z-index: 1; min-height: 0; // Absolute must for Firefox to scroll contents +} + +.mapEmbeddedError { + flex-grow: 1; + display: flex; + align-items: center; + justify-content: center; + overflow: auto; } \ No newline at end of file diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index c15138f6c5b15..b0daace7afa9e 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -12,6 +12,7 @@ import { Provider } from 'react-redux'; import { render, unmountComponentAtNode } from 'react-dom'; import { Subscription } from 'rxjs'; import { Unsubscribe } from 'redux'; +import { EuiEmptyPrompt } from '@elastic/eui'; import { Embeddable, IContainer, @@ -66,6 +67,7 @@ import { getCoreI18n, getHttp, getChartsPaletteServiceGetColor, + getSpacesApi, getSearchService, } from '../kibana_services'; import { LayerDescriptor, MapExtent } from '../../common/descriptor_types'; @@ -353,23 +355,38 @@ export class MapEmbeddable return; } - const I18nContext = getCoreI18n().Context; + const sharingSavedObjectProps = this._savedMap.getSharingSavedObjectProps(); + const spaces = getSpacesApi(); + const content = + sharingSavedObjectProps && spaces && sharingSavedObjectProps?.outcome === 'conflict' ? ( + <div className="mapEmbeddedError"> + <EuiEmptyPrompt + iconType="alert" + iconColor="danger" + data-test-subj="embeddable-maps-failure" + body={spaces.ui.components.getSavedObjectConflictMessage({ + json: sharingSavedObjectProps.errorJSON!, + })} + /> + </div> + ) : ( + <MapContainer + onSingleValueTrigger={this.onSingleValueTrigger} + addFilters={this.input.hideFilterActions ? null : this.addFilters} + getFilterActions={this.getFilterActions} + getActionContext={this.getActionContext} + renderTooltipContent={this._renderTooltipContent} + title={this.getTitle()} + description={this.getDescription()} + waitUntilTimeLayersLoad$={waitUntilTimeLayersLoad$(this._savedMap.getStore())} + isSharable={this._isSharable} + /> + ); + const I18nContext = getCoreI18n().Context; render( <Provider store={this._savedMap.getStore()}> - <I18nContext> - <MapContainer - onSingleValueTrigger={this.onSingleValueTrigger} - addFilters={this.input.hideFilterActions ? null : this.addFilters} - getFilterActions={this.getFilterActions} - getActionContext={this.getActionContext} - renderTooltipContent={this._renderTooltipContent} - title={this.getTitle()} - description={this.getDescription()} - waitUntilTimeLayersLoad$={waitUntilTimeLayersLoad$(this._savedMap.getStore())} - isSharable={this._isSharable} - /> - </I18nContext> + <I18nContext>{content}</I18nContext> </Provider>, this._domNode ); diff --git a/x-pack/plugins/maps/public/inspector/adapters/map_adapter.js b/x-pack/plugins/maps/public/inspector/map_adapter.ts similarity index 78% rename from x-pack/plugins/maps/public/inspector/adapters/map_adapter.js rename to x-pack/plugins/maps/public/inspector/map_adapter.ts index 44561ff6d66ff..3097f686382ac 100644 --- a/x-pack/plugins/maps/public/inspector/adapters/map_adapter.js +++ b/x-pack/plugins/maps/public/inspector/map_adapter.ts @@ -6,9 +6,13 @@ */ import { EventEmitter } from 'events'; +import { Stats } from './types'; class MapAdapter extends EventEmitter { - setMapState({ stats, style }) { + private stats?: Stats; + private style?: string; + + setMapState({ stats, style }: { stats: Stats; style: string }) { this.stats = stats; this.style = style; this._onChange(); diff --git a/x-pack/plugins/maps/public/inspector/views/map_details.js b/x-pack/plugins/maps/public/inspector/map_details.tsx similarity index 74% rename from x-pack/plugins/maps/public/inspector/views/map_details.js rename to x-pack/plugins/maps/public/inspector/map_details.tsx index d477e55270eb5..6689cb0d6ccb4 100644 --- a/x-pack/plugins/maps/public/inspector/views/map_details.js +++ b/x-pack/plugins/maps/public/inspector/map_details.tsx @@ -6,7 +6,6 @@ */ import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { EuiTab, EuiTabs, @@ -22,31 +21,42 @@ import { FormattedMessage } from '@kbn/i18n/react'; const DETAILS_TAB_ID = 'details'; const STYLE_TAB_ID = 'mapStyle'; -class MapDetails extends Component { - tabs = [ - { - id: DETAILS_TAB_ID, - name: i18n.translate('xpack.maps.inspector.mapDetailsTitle', { - defaultMessage: 'Map details', - }), - dataTestSubj: 'mapDetailsTab', - }, - { - id: STYLE_TAB_ID, - name: i18n.translate('xpack.maps.inspector.mapboxStyleTitle', { - defaultMessage: 'Mapbox style', - }), - dataTestSubj: 'mapboxStyleTab', - }, - ]; +const TABS = [ + { + id: DETAILS_TAB_ID, + name: i18n.translate('xpack.maps.inspector.mapDetailsTitle', { + defaultMessage: 'Map details', + }), + dataTestSubj: 'mapDetailsTab', + }, + { + id: STYLE_TAB_ID, + name: i18n.translate('xpack.maps.inspector.mapboxStyleTitle', { + defaultMessage: 'Mapbox style', + }), + dataTestSubj: 'mapboxStyleTab', + }, +]; - state = { +interface Props { + centerLon: number; + centerLat: number; + zoom: number; + style: string; +} + +interface State { + selectedTabId: typeof DETAILS_TAB_ID | typeof STYLE_TAB_ID; +} + +export class MapDetails extends Component<Props, State> { + state: State = { selectedTabId: DETAILS_TAB_ID, }; - onSelectedTabChanged = (id) => { + onSelectedTabChanged = (id: string) => { this.setState({ - selectedTabId: id, + selectedTabId: id as typeof DETAILS_TAB_ID | typeof STYLE_TAB_ID, }); }; @@ -55,7 +65,7 @@ class MapDetails extends Component { return ( <div data-test-subj="mapboxStyleContainer"> <EuiCodeBlock language="json" paddingSize="s"> - {JSON.stringify(this.props.mapStyle, null, 2)} + {JSON.stringify(this.props.style, null, 2)} </EuiCodeBlock> </div> ); @@ -96,7 +106,7 @@ class MapDetails extends Component { }; renderTabs() { - return this.tabs.map((tab, index) => ( + return TABS.map((tab, index) => ( <EuiTab onClick={() => this.onSelectedTabChanged(tab.id)} isSelected={tab.id === this.state.selectedTabId} @@ -118,12 +128,3 @@ class MapDetails extends Component { ); } } - -MapDetails.propTypes = { - centerLon: PropTypes.number.isRequired, - centerLat: PropTypes.number.isRequired, - zoom: PropTypes.number.isRequired, - mapStyle: PropTypes.object.isRequired, -}; - -export { MapDetails }; diff --git a/x-pack/plugins/maps/public/inspector/map_inspector_view.tsx b/x-pack/plugins/maps/public/inspector/map_inspector_view.tsx new file mode 100644 index 0000000000000..77809b81b9077 --- /dev/null +++ b/x-pack/plugins/maps/public/inspector/map_inspector_view.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { lazy } from 'react'; +import type { Adapters } from 'src/plugins/inspector/public'; +import { i18n } from '@kbn/i18n'; +import { LazyWrapper } from '../lazy_wrapper'; + +const getLazyComponent = () => { + return lazy(() => import('./map_view_component')); +}; + +export const MapInspectorView = { + title: i18n.translate('xpack.maps.inspector.mapDetailsViewTitle', { + defaultMessage: 'Map details', + }), + order: 30, + help: i18n.translate('xpack.maps.inspector.mapDetailsViewHelpText', { + defaultMessage: 'View the map state', + }), + shouldShow(adapters: Adapters) { + return Boolean(adapters.map); + }, + component: (props: { adapters: Adapters }) => { + return <LazyWrapper getLazyComponent={getLazyComponent} lazyComponentProps={props} />; + }, +}; diff --git a/x-pack/plugins/maps/public/inspector/map_view_component.tsx b/x-pack/plugins/maps/public/inspector/map_view_component.tsx new file mode 100644 index 0000000000000..364f4d4d2d435 --- /dev/null +++ b/x-pack/plugins/maps/public/inspector/map_view_component.tsx @@ -0,0 +1,51 @@ +/* + * 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, { Component } from 'react'; +import type { Adapters } from 'src/plugins/inspector/public'; +import { MapDetails } from './map_details'; +import { Stats } from './types'; + +interface Props { + adapters: Adapters; +} + +interface State { + stats: Stats; + style: string; +} + +class MapViewComponent extends Component<Props, State> { + state: State = this.props.adapters.map.getMapState(); + + _onMapChange = () => { + this.setState(this.props.adapters.map.getMapState()); + }; + + componentDidMount() { + this.props.adapters.map.on('change', this._onMapChange); + } + + componentWillUnmount() { + this.props.adapters.map.removeListener('change', this._onMapChange); + } + + render() { + return ( + <MapDetails + centerLon={this.state.stats.center[0]} + centerLat={this.state.stats.center[1]} + zoom={this.state.stats.zoom} + style={this.state.style} + /> + ); + } +} + +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export default MapViewComponent; diff --git a/x-pack/plugins/maps/public/inspector/types.ts b/x-pack/plugins/maps/public/inspector/types.ts new file mode 100644 index 0000000000000..e8bbd126cdd08 --- /dev/null +++ b/x-pack/plugins/maps/public/inspector/types.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface Stats { + center: [number, number]; + zoom: number; +} diff --git a/x-pack/plugins/maps/public/inspector/views/map_view.js b/x-pack/plugins/maps/public/inspector/views/map_view.js deleted file mode 100644 index e2aac26dc7d17..0000000000000 --- a/x-pack/plugins/maps/public/inspector/views/map_view.js +++ /dev/null @@ -1,67 +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, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { MapDetails } from './map_details'; -import { i18n } from '@kbn/i18n'; - -class MapViewComponent extends Component { - constructor(props) { - super(props); - props.adapters.map.on('change', this._onMapChange); - - const { stats, style } = props.adapters.map.getMapState(); - this.state = { - stats, - mapStyle: style, - }; - } - - _onMapChange = () => { - const { stats, style } = this.props.adapters.map.getMapState(); - this.setState({ - stats, - mapStyle: style, - }); - }; - - componentWillUnmount() { - this.props.adapters.map.removeListener('change', this._onMapChange); - } - - render() { - return ( - <MapDetails - centerLon={this.state.stats.center[0]} - centerLat={this.state.stats.center[1]} - zoom={this.state.stats.zoom} - mapStyle={this.state.mapStyle} - /> - ); - } -} - -MapViewComponent.propTypes = { - adapters: PropTypes.object.isRequired, -}; - -const MapView = { - title: i18n.translate('xpack.maps.inspector.mapDetailsViewTitle', { - defaultMessage: 'Map details', - }), - order: 30, - help: i18n.translate('xpack.maps.inspector.mapDetailsViewHelpText', { - defaultMessage: 'View the map state', - }), - shouldShow(adapters) { - return Boolean(adapters.map); - }, - component: MapViewComponent, -}; - -export { MapView }; diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index 300fe07a841e9..5ad3a1d3fd23d 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -53,6 +53,7 @@ export const getNavigateToApp = () => coreStart.application.navigateToApp; export const getSavedObjectsTagging = () => pluginsStart.savedObjectsTagging; export const getPresentationUtilContext = () => pluginsStart.presentationUtil.ContextProvider; export const getSecurityService = () => pluginsStart.security; +export const getSpacesApi = () => pluginsStart.spaces; // xpack.maps.* kibana.yml settings from this plugin let mapAppConfig: MapsConfigType; diff --git a/x-pack/plugins/maps/public/lazy_wrapper.tsx b/x-pack/plugins/maps/public/lazy_wrapper.tsx new file mode 100644 index 0000000000000..1a808799bf4c4 --- /dev/null +++ b/x-pack/plugins/maps/public/lazy_wrapper.tsx @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, Suspense } from 'react'; +import { EuiDelayRender, EuiErrorBoundary, EuiLoadingContent } from '@elastic/eui'; + +const Fallback = () => ( + <EuiDelayRender> + <EuiLoadingContent lines={3} /> + </EuiDelayRender> +); + +interface Props<T> { + getLazyComponent: () => FC<T>; + lazyComponentProps: T; +} + +export function LazyWrapper<T>({ getLazyComponent, lazyComponentProps }: Props<T>) { + const LazyComponent = getLazyComponent(); + return ( + <EuiErrorBoundary> + <Suspense fallback={<Fallback />}> + <LazyComponent {...lazyComponentProps} /> + </Suspense> + </EuiErrorBoundary> + ); +} diff --git a/x-pack/plugins/maps/public/map_attribute_service.ts b/x-pack/plugins/maps/public/map_attribute_service.ts index 5f7c45b1b42d7..ab380ca5a6b66 100644 --- a/x-pack/plugins/maps/public/map_attribute_service.ts +++ b/x-pack/plugins/maps/public/map_attribute_service.ts @@ -14,8 +14,18 @@ import { checkForDuplicateTitle, OnSaveProps } from '../../../../src/plugins/sav import { getCoreOverlays, getEmbeddableService, getSavedObjectsClient } from './kibana_services'; import { extractReferences, injectReferences } from '../common/migrations/references'; import { MapByValueInput, MapByReferenceInput } from './embeddable/types'; +import { getSpacesApi } from './kibana_services'; -type MapDoc = MapSavedObjectAttributes & { references?: SavedObjectReference[] }; +export interface SharingSavedObjectProps { + outcome?: 'aliasMatch' | 'exactMatch' | 'conflict'; + aliasTargetId?: string; + errorJSON?: string; +} + +type MapDoc = MapSavedObjectAttributes & { + sharingSavedObjectProps?: SharingSavedObjectProps; + references?: SavedObjectReference[]; +}; export type MapAttributeService = AttributeService<MapDoc, MapByValueInput, MapByReferenceInput>; @@ -58,7 +68,11 @@ export function getMapAttributeService(): MapAttributeService { return { id: savedObject.id }; }, unwrapMethod: async (savedObjectId: string): Promise<MapDoc> => { - const savedObject = await getSavedObjectsClient().get<MapSavedObjectAttributes>( + const { + saved_object: savedObject, + outcome, + alias_target_id: aliasTargetId, + } = await getSavedObjectsClient().resolve<MapSavedObjectAttributes>( MAP_SAVED_OBJECT_TYPE, savedObjectId ); @@ -68,7 +82,22 @@ export function getMapAttributeService(): MapAttributeService { } const { attributes } = injectReferences(savedObject); - return { ...attributes, references: savedObject.references }; + return { + ...attributes, + references: savedObject.references, + sharingSavedObjectProps: { + aliasTargetId, + outcome, + errorJSON: + outcome === 'conflict' && getSpacesApi() + ? JSON.stringify({ + targetType: MAP_SAVED_OBJECT_TYPE, + sourceId: savedObjectId, + targetSpace: (await getSpacesApi()!.getActiveSpace()).id, + }) + : undefined, + }, + }; }, checkForDuplicateTitle: (props: OnSaveProps) => { return checkForDuplicateTitle( diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 8f6e74adfc2fd..ee3202ba022c9 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -20,8 +20,7 @@ import type { PluginInitializerContext, } from '../../../../src/core/public'; import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; -// @ts-ignore -import { MapView } from './inspector/views/map_view'; +import { MapInspectorView } from './inspector/map_inspector_view'; import { setEMSSettings, setKibanaCommonConfig, @@ -83,7 +82,8 @@ import { tileMapRenderer, tileMapVisType, } from './legacy_visualizations'; -import { SecurityPluginStart } from '../../security/public'; +import type { SecurityPluginStart } from '../../security/public'; +import type { SpacesPluginStart } from '../../spaces/public'; export interface MapsPluginSetupDependencies { expressions: ReturnType<ExpressionsPublicPlugin['setup']>; @@ -113,6 +113,7 @@ export interface MapsPluginStartDependencies { savedObjectsTagging?: SavedObjectTaggingPluginStart; presentationUtil: PresentationUtilPluginStart; security: SecurityPluginStart; + spaces?: SpacesPluginStart; } /** @@ -166,7 +167,7 @@ export class MapsPlugin }) ); - plugins.inspector.registerView(MapView); + plugins.inspector.registerView(MapInspectorView); if (plugins.home) { plugins.home.featureCatalogue.register(featureCatalogueEntry); } diff --git a/x-pack/plugins/maps/public/reducers/non_serializable_instances.js b/x-pack/plugins/maps/public/reducers/non_serializable_instances.js index 402d7727cd6fe..9524d25a9a476 100644 --- a/x-pack/plugins/maps/public/reducers/non_serializable_instances.js +++ b/x-pack/plugins/maps/public/reducers/non_serializable_instances.js @@ -6,7 +6,7 @@ */ import { RequestAdapter } from '../../../../../src/plugins/inspector/common/adapters/request'; -import { MapAdapter } from '../inspector/adapters/map_adapter'; +import { MapAdapter } from '../inspector/map_adapter'; import { getShowMapsInspectorAdapter } from '../kibana_services'; const REGISTER_CANCEL_CALLBACK = 'REGISTER_CANCEL_CALLBACK'; diff --git a/x-pack/plugins/maps/public/render_app.tsx b/x-pack/plugins/maps/public/render_app.tsx index de67939b1a42a..3eefaeb6f7a9b 100644 --- a/x-pack/plugins/maps/public/render_app.tsx +++ b/x-pack/plugins/maps/public/render_app.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { Router, Switch, Route, Redirect, RouteComponentProps } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; -import { AppMountParameters } from 'kibana/public'; +import type { AppMountParameters } from 'kibana/public'; import { getCoreChrome, getCoreI18n, @@ -98,6 +98,7 @@ export async function renderApp( setHeaderActionMenu={setHeaderActionMenu} stateTransfer={stateTransfer} originatingApp={originatingApp} + history={history} key={routeProps.match.params.savedMapId ? routeProps.match.params.savedMapId : 'new'} /> ); diff --git a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx index 212fa89e2ad65..8fc2d97c4862a 100644 --- a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx @@ -18,6 +18,7 @@ import { getCoreChrome, getMapsCapabilities, getNavigation, + getSpacesApi, getTimeFilter, getToasts, } from '../../../kibana_services'; @@ -40,7 +41,8 @@ import { getIndexPatternsFromIds } from '../../../index_pattern_util'; import { getTopNavConfig } from '../top_nav_config'; import { goToSpecifiedPath } from '../../../render_app'; import { MapSavedObjectAttributes } from '../../../../common/map_saved_object_type'; -import { getFullPath, APP_ID } from '../../../../common/constants'; +import { getEditPath, getFullPath, APP_ID } from '../../../../common/constants'; +import { getMapEmbeddableDisplayName } from '../../../../common/i18n_getters'; import { getInitialQuery, getInitialRefreshConfig, @@ -85,6 +87,7 @@ export interface Props { isSaveDisabled: boolean; query: Query | undefined; setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; + history: AppMountParameters['history']; } export interface State { @@ -347,6 +350,16 @@ export class MapApp extends React.Component<Props, State> { return; } + const sharingSavedObjectProps = this.props.savedMap.getSharingSavedObjectProps(); + const spaces = getSpacesApi(); + if (spaces && sharingSavedObjectProps?.outcome === 'aliasMatch') { + // We found this object by a legacy URL alias from its old ID; redirect the user to the page with its new ID, preserving any URL hash + const newObjectId = sharingSavedObjectProps?.aliasTargetId; // This is always defined if outcome === 'aliasMatch' + const newPath = `${getEditPath(newObjectId)}${this.props.history.location.hash}`; + await spaces.ui.redirectLegacyUrl(newPath, getMapEmbeddableDisplayName()); + return; + } + this.props.savedMap.setBreadcrumbs(); getCoreChrome().docTitle.change(this.props.savedMap.getTitle()); const savedObjectId = this.props.savedMap.getSavedObjectId(); @@ -437,6 +450,21 @@ export class MapApp extends React.Component<Props, State> { this._onFiltersChange([...this.props.filters, ...newFilters]); }; + _renderLegacyUrlConflict() { + const sharingSavedObjectProps = this.props.savedMap.getSharingSavedObjectProps(); + const spaces = getSpacesApi(); + return spaces && sharingSavedObjectProps?.outcome === 'conflict' + ? spaces.ui.components.getLegacyUrlConflict({ + objectNoun: getMapEmbeddableDisplayName(), + currentObjectId: this.props.savedMap.getSavedObjectId()!, + otherObjectId: sharingSavedObjectProps.aliasTargetId!, + otherObjectPath: `${getEditPath(sharingSavedObjectProps.aliasTargetId!)}${ + this.props.history.location.hash + }`, + }) + : null; + } + render() { if (!this.state.initialized) { return null; @@ -447,6 +475,7 @@ export class MapApp extends React.Component<Props, State> { {this._renderTopNav()} <h1 className="euiScreenReaderOnly">{`screenTitle placeholder`}</h1> <div id="react-maps-root"> + {this._renderLegacyUrlConflict()} <MapContainer addFilters={this._addFilter} title={this.props.savedMap.getAttributes().title} diff --git a/x-pack/plugins/maps/public/routes/map_page/map_page.tsx b/x-pack/plugins/maps/public/routes/map_page/map_page.tsx index 73ea62ef59d7c..7e927115a5d06 100644 --- a/x-pack/plugins/maps/public/routes/map_page/map_page.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/map_page.tsx @@ -7,8 +7,8 @@ import React, { Component } from 'react'; import { Provider } from 'react-redux'; -import { AppMountParameters } from 'kibana/public'; -import { EmbeddableStateTransfer } from 'src/plugins/embeddable/public'; +import type { AppMountParameters } from 'kibana/public'; +import type { EmbeddableStateTransfer } from 'src/plugins/embeddable/public'; import { MapApp } from './map_app'; import { SavedMap, getInitialLayersFromUrlParam } from './saved_map'; import { MapEmbeddableInput } from '../../embeddable/types'; @@ -20,6 +20,7 @@ interface Props { setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; stateTransfer: EmbeddableStateTransfer; originatingApp?: string; + history: AppMountParameters['history']; } interface State { @@ -69,6 +70,7 @@ export class MapPage extends Component<Props, State> { return ( <Provider store={this.state.savedMap.getStore()}> <MapApp + history={this.props.history} savedMap={this.state.savedMap} onAppLeave={this.props.onAppLeave} setHeaderActionMenu={this.props.setHeaderActionMenu} diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts index fab88af308f8d..004b88a242623 100644 --- a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts @@ -30,7 +30,7 @@ import { setHiddenLayers, } from '../../../actions'; import { getIsLayerTOCOpen, getOpenTOCDetails } from '../../../selectors/ui_selectors'; -import { getMapAttributeService } from '../../../map_attribute_service'; +import { getMapAttributeService, SharingSavedObjectProps } from '../../../map_attribute_service'; import { OnSaveProps } from '../../../../../../../src/plugins/saved_objects/public'; import { MapByReferenceInput, MapEmbeddableInput } from '../../../embeddable/types'; import { @@ -50,6 +50,7 @@ import { whenLicenseInitialized } from '../../../licensed_features'; export class SavedMap { private _attributes: MapSavedObjectAttributes | null = null; + private _sharingSavedObjectProps: SharingSavedObjectProps | null = null; private readonly _defaultLayers: LayerDescriptor[]; private readonly _embeddableId?: string; private _initialLayerListConfig: LayerDescriptor[] = []; @@ -98,8 +99,11 @@ export class SavedMap { }; } else { const doc = await getMapAttributeService().unwrapAttributes(this._mapEmbeddableInput); - const { references, ...savedObjectAttributes } = doc; + const { references, sharingSavedObjectProps, ...savedObjectAttributes } = doc; this._attributes = savedObjectAttributes; + if (sharingSavedObjectProps) { + this._sharingSavedObjectProps = sharingSavedObjectProps; + } const savedObjectsTagging = getSavedObjectsTagging(); if (savedObjectsTagging && references && references.length) { this._tags = savedObjectsTagging.ui.getTagIdsFromReferences(references); @@ -274,6 +278,10 @@ export class SavedMap { return this._attributes; } + public getSharingSavedObjectProps(): SharingSavedObjectProps | null { + return this._sharingSavedObjectProps; + } + public isByValue(): boolean { const hasSavedObjectId = !!this.getSavedObjectId(); return getIsAllowByValueEmbeddables() && !!this._originatingApp && !hasSavedObjectId; diff --git a/x-pack/plugins/maps/tsconfig.json b/x-pack/plugins/maps/tsconfig.json index 3f6e1fdbe8475..e2be2f3d66561 100644 --- a/x-pack/plugins/maps/tsconfig.json +++ b/x-pack/plugins/maps/tsconfig.json @@ -36,6 +36,7 @@ { "path": "../licensing/tsconfig.json" }, { "path": "../file_upload/tsconfig.json" }, { "path": "../saved_objects_tagging/tsconfig.json" }, - { "path": "../security/tsconfig.json" } + { "path": "../security/tsconfig.json" }, + { "path": "../spaces/tsconfig.json" } ] } diff --git a/x-pack/plugins/reporting/common/constants.ts b/x-pack/plugins/reporting/common/constants.ts index 9224a23fcb33f..3fb02677dd981 100644 --- a/x-pack/plugins/reporting/common/constants.ts +++ b/x-pack/plugins/reporting/common/constants.ts @@ -7,6 +7,8 @@ export const PLUGIN_ID = 'reporting'; +export const REPORTING_SYSTEM_INDEX = '.reporting'; + export const JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY = 'xpack.reporting.jobCompletionNotifications'; diff --git a/x-pack/plugins/reporting/server/config/schema.test.ts b/x-pack/plugins/reporting/server/config/schema.test.ts index 0998a80103131..0b2e2cac6ff7c 100644 --- a/x-pack/plugins/reporting/server/config/schema.test.ts +++ b/x-pack/plugins/reporting/server/config/schema.test.ts @@ -84,7 +84,6 @@ describe('Reporting Config Schema', () => { }, "enabled": true, "encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "index": ".reporting", "kibanaServer": Object {}, "poll": Object { "jobCompletionNotifier": Object { @@ -189,7 +188,6 @@ describe('Reporting Config Schema', () => { "useByteOrderMarkEncoding": false, }, "enabled": true, - "index": ".reporting", "kibanaServer": Object {}, "poll": Object { "jobCompletionNotifier": Object { diff --git a/x-pack/plugins/reporting/server/config/schema.ts b/x-pack/plugins/reporting/server/config/schema.ts index d616a18289df0..affd8b7bee7ff 100644 --- a/x-pack/plugins/reporting/server/config/schema.ts +++ b/x-pack/plugins/reporting/server/config/schema.ts @@ -155,8 +155,6 @@ const RolesSchema = schema.object({ allow: schema.arrayOf(schema.string(), { defaultValue: ['reporting_user'] }), }); -const IndexSchema = schema.string({ defaultValue: '.reporting' }); - // Browser side polling: job completion notifier, management table auto-refresh // NOTE: can not use schema.duration, a bug prevents it being passed to the browser correctly const PollSchema = schema.object({ @@ -178,7 +176,6 @@ export const ConfigSchema = schema.object({ csv: CsvSchema, encryptionKey: EncryptionKeySchema, roles: RolesSchema, - index: IndexSchema, poll: PollSchema, }); diff --git a/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts index 5032eaab46e84..e5d0ed2613719 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts @@ -77,7 +77,6 @@ describe('CSV Execute Job', function () { stream = { write: jest.fn((chunk) => (content += chunk)) } as unknown as typeof stream; configGetStub = sinon.stub(); configGetStub.withArgs('queue', 'timeout').returns(moment.duration('2m')); - configGetStub.withArgs('index').returns('.reporting-foo-test'); configGetStub.withArgs('encryptionKey').returns(encryptionKey); configGetStub.withArgs('csv', 'maxSizeBytes').returns(1024 * 1000); // 1mB configGetStub.withArgs('csv', 'scroll').returns({}); diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts index d49337391ca40..01a6f7a3cd06d 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.ts @@ -9,7 +9,7 @@ import { IndexResponse, UpdateResponse } from '@elastic/elasticsearch/api/types' import { ElasticsearchClient } from 'src/core/server'; import { LevelLogger, statuses } from '../'; import { ReportingCore } from '../../'; -import { ILM_POLICY_NAME } from '../../../common/constants'; +import { ILM_POLICY_NAME, REPORTING_SYSTEM_INDEX } from '../../../common/constants'; import { JobStatus, ReportOutput, ReportSource } from '../../../common/types'; import { ReportTaskParams } from '../tasks'; import { Report, ReportDocument, SavedReport } from './'; @@ -87,7 +87,7 @@ export class ReportingStore { constructor(private reportingCore: ReportingCore, private logger: LevelLogger) { const config = reportingCore.getConfig(); - this.indexPrefix = config.get('index'); + this.indexPrefix = REPORTING_SYSTEM_INDEX; this.indexInterval = config.get('queue', 'indexInterval'); this.logger = logger.clone(['store']); } diff --git a/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts b/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts index afa83ed331672..54efe0636536a 100644 --- a/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts +++ b/x-pack/plugins/reporting/server/routes/lib/jobs_query.ts @@ -13,6 +13,7 @@ import { UnwrapPromise } from '@kbn/utility-types'; import { ElasticsearchClient } from 'src/core/server'; import { PromiseType } from 'utility-types'; import { ReportingCore } from '../../'; +import { REPORTING_SYSTEM_INDEX } from '../../../common/constants'; import { ReportApiJSON, ReportSource } from '../../../common/types'; import { statuses } from '../../lib/statuses'; import { Report } from '../../lib/store'; @@ -54,9 +55,7 @@ interface JobsQueryFactory { export function jobsQueryFactory(reportingCore: ReportingCore): JobsQueryFactory { function getIndex() { - const config = reportingCore.getConfig(); - - return `${config.get('index')}-*`; + return `${REPORTING_SYSTEM_INDEX}-*`; } async function execQuery< diff --git a/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts b/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts index 9a452943ff699..69213d8f8cacc 100644 --- a/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts +++ b/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts @@ -8,6 +8,7 @@ import type { estypes } from '@elastic/elasticsearch'; import type { ElasticsearchClient } from 'kibana/server'; import { get } from 'lodash'; import type { ReportingConfig } from '../'; +import { REPORTING_SYSTEM_INDEX } from '../../common/constants'; import type { ExportTypesRegistry } from '../lib/export_types_registry'; import type { GetLicense } from './'; import { getExportStats } from './get_export_stats'; @@ -144,7 +145,7 @@ export async function getReportingUsage( esClient: ElasticsearchClient, exportTypesRegistry: ExportTypesRegistry ): Promise<ReportingUsageType> { - const reportingIndex = config.get('index'); + const reportingIndex = REPORTING_SYSTEM_INDEX; const params = { index: `${reportingIndex}-*`, filterPath: 'aggregations.*.buckets', diff --git a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts index 54a4b80a35bb4..1c59e56c0466a 100644 --- a/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts +++ b/x-pack/plugins/rule_registry/common/assets/field_maps/technical_rule_field_map.ts @@ -16,6 +16,7 @@ export const technicalRuleFieldMap = { Fields.EVENT_ACTION, Fields.TAGS ), + [Fields.ALERT_RULE_PARAMS]: { type: 'keyword', index: false }, [Fields.ALERT_RULE_TYPE_ID]: { type: 'keyword', required: true }, [Fields.ALERT_RULE_CONSUMER]: { type: 'keyword', required: true }, [Fields.ALERT_RULE_PRODUCER]: { type: 'keyword', required: true }, diff --git a/x-pack/plugins/security/common/model/deprecations.ts b/x-pack/plugins/security/common/model/deprecations.ts new file mode 100644 index 0000000000000..e990f370c5173 --- /dev/null +++ b/x-pack/plugins/security/common/model/deprecations.ts @@ -0,0 +1,25 @@ +/* + * 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-next-line @kbn/eslint/no-restricted-paths +import type { DeprecationsDetails, GetDeprecationsContext } from '../../../../../src/core/server'; +import type { Role } from './role'; + +export interface PrivilegeDeprecationsRolesByFeatureIdResponse { + roles?: Role[]; + errors?: DeprecationsDetails[]; +} + +export interface PrivilegeDeprecationsRolesByFeatureIdRequest { + context: GetDeprecationsContext; + featureId: string; +} +export interface PrivilegeDeprecationsService { + getKibanaRolesByFeatureId: ( + args: PrivilegeDeprecationsRolesByFeatureIdRequest + ) => Promise<PrivilegeDeprecationsRolesByFeatureIdResponse>; +} diff --git a/x-pack/plugins/security/common/model/index.ts b/x-pack/plugins/security/common/model/index.ts index 8eb341ef9bd37..082e6bdc12cd0 100644 --- a/x-pack/plugins/security/common/model/index.ts +++ b/x-pack/plugins/security/common/model/index.ts @@ -33,3 +33,8 @@ export { RoleTemplate, RoleMapping, } from './role_mapping'; +export { + PrivilegeDeprecationsRolesByFeatureIdRequest, + PrivilegeDeprecationsRolesByFeatureIdResponse, + PrivilegeDeprecationsService, +} from './deprecations'; diff --git a/x-pack/plugins/security/server/authorization/index.ts b/x-pack/plugins/security/server/authorization/index.ts index 4d67f3435e7da..221baa85a65f6 100644 --- a/x-pack/plugins/security/server/authorization/index.ts +++ b/x-pack/plugins/security/server/authorization/index.ts @@ -13,3 +13,4 @@ export { } from './authorization_service'; export { CheckSavedObjectsPrivileges } from './check_saved_objects_privileges'; export { CheckPrivilegesPayload } from './types'; +export { transformElasticsearchRoleToRole, ElasticsearchRole } from './roles'; diff --git a/x-pack/plugins/security/server/routes/authorization/roles/model/elasticsearch_role.ts b/x-pack/plugins/security/server/authorization/roles/elasticsearch_role.ts similarity index 96% rename from x-pack/plugins/security/server/routes/authorization/roles/model/elasticsearch_role.ts rename to x-pack/plugins/security/server/authorization/roles/elasticsearch_role.ts index fa119ca704753..c0dab16f97af8 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/model/elasticsearch_role.ts +++ b/x-pack/plugins/security/server/authorization/roles/elasticsearch_role.ts @@ -8,10 +8,10 @@ import { GLOBAL_RESOURCE, RESERVED_PRIVILEGES_APPLICATION_WILDCARD, -} from '../../../../../common/constants'; -import type { Role, RoleKibanaPrivilege } from '../../../../../common/model'; -import { PrivilegeSerializer } from '../../../../authorization/privilege_serializer'; -import { ResourceSerializer } from '../../../../authorization/resource_serializer'; +} from '../../../common/constants'; +import type { Role, RoleKibanaPrivilege } from '../../../common/model'; +import { PrivilegeSerializer } from '../privilege_serializer'; +import { ResourceSerializer } from '../resource_serializer'; export type ElasticsearchRole = Pick<Role, 'name' | 'metadata' | 'transient_metadata'> & { applications: Array<{ diff --git a/x-pack/plugins/security/server/authorization/roles/index.ts b/x-pack/plugins/security/server/authorization/roles/index.ts new file mode 100644 index 0000000000000..a5047a1872c09 --- /dev/null +++ b/x-pack/plugins/security/server/authorization/roles/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { transformElasticsearchRoleToRole, ElasticsearchRole } from './elasticsearch_role'; diff --git a/x-pack/plugins/security/server/deprecations/index.ts b/x-pack/plugins/security/server/deprecations/index.ts new file mode 100644 index 0000000000000..05802a5a673c5 --- /dev/null +++ b/x-pack/plugins/security/server/deprecations/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * getKibanaRolesByFeature + */ + +export { getPrivilegeDeprecationsService } from './privilege_deprecations'; diff --git a/x-pack/plugins/security/server/deprecations/privilege_deprecations.test.ts b/x-pack/plugins/security/server/deprecations/privilege_deprecations.test.ts new file mode 100644 index 0000000000000..e889eb17d5af9 --- /dev/null +++ b/x-pack/plugins/security/server/deprecations/privilege_deprecations.test.ts @@ -0,0 +1,284 @@ +/* + * 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 { GetDeprecationsContext } from 'src/core/server'; +import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; + +import { getPrivilegeDeprecationsService } from '.'; +import { licenseMock } from '../../common/licensing/index.mock'; + +const kibanaIndexName = '.a-kibana-index'; +const application = `kibana-${kibanaIndexName}`; + +describe('#getPrivilegeDeprecationsService', () => { + describe('#getKibanaRolesByFeatureId', () => { + const mockAsCurrentUser = elasticsearchServiceMock.createScopedClusterClient(); + const mockLicense = licenseMock.create(); + const mockLogger = loggingSystemMock.createLogger(); + const authz = { applicationName: application }; + + const { getKibanaRolesByFeatureId } = getPrivilegeDeprecationsService( + authz, + mockLicense, + mockLogger + ); + + it('happy path to find siem roles with feature_siem privileges', async () => { + mockAsCurrentUser.asCurrentUser.security.getRole.mockResolvedValue( + elasticsearchServiceMock.createSuccessTransportRequestPromise({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: ['feature_siem.all', 'feature_siem.cases_read'], + resources: ['space:securitySolutions'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }) + ); + + const mockContext = { + esClient: mockAsCurrentUser, + savedObjectsClient: jest.fn(), + } as unknown as GetDeprecationsContext; + + const resp = await getKibanaRolesByFeatureId({ context: mockContext, featureId: 'siem' }); + expect(resp).toMatchInlineSnapshot(` + Object { + "roles": Array [ + Object { + "_transform_error": Array [], + "_unrecognized_applications": Array [], + "elasticsearch": Object { + "cluster": Array [], + "indices": Array [], + "run_as": Array [], + }, + "kibana": Array [ + Object { + "base": Array [], + "feature": Object { + "siem": Array [ + "all", + "cases_read", + ], + }, + "spaces": Array [ + "securitySolutions", + ], + }, + ], + "metadata": Object { + "_reserved": true, + }, + "name": "first_role", + "transient_metadata": Object { + "enabled": true, + }, + }, + ], + } + `); + }); + + it('happy path to find siem roles with feature_siem and feature_foo and feature_bar privileges', async () => { + mockAsCurrentUser.asCurrentUser.security.getRole.mockResolvedValue( + elasticsearchServiceMock.createSuccessTransportRequestPromise({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: [ + 'feature_foo.foo-privilege-1', + 'feature_foo.foo-privilege-2', + 'feature_bar.bar-privilege-1', + 'feature_siem.all', + 'feature_siem.cases_read', + ], + resources: ['space:securitySolutions'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }) + ); + + const mockContext = { + esClient: mockAsCurrentUser, + savedObjectsClient: jest.fn(), + } as unknown as GetDeprecationsContext; + + const resp = await getKibanaRolesByFeatureId({ context: mockContext, featureId: 'siem' }); + expect(resp).toMatchInlineSnapshot(` + Object { + "roles": Array [ + Object { + "_transform_error": Array [], + "_unrecognized_applications": Array [], + "elasticsearch": Object { + "cluster": Array [], + "indices": Array [], + "run_as": Array [], + }, + "kibana": Array [ + Object { + "base": Array [], + "feature": Object { + "bar": Array [ + "bar-privilege-1", + ], + "foo": Array [ + "foo-privilege-1", + "foo-privilege-2", + ], + "siem": Array [ + "all", + "cases_read", + ], + }, + "spaces": Array [ + "securitySolutions", + ], + }, + ], + "metadata": Object { + "_reserved": true, + }, + "name": "first_role", + "transient_metadata": Object { + "enabled": true, + }, + }, + ], + } + `); + }); + + it('happy path to NOT find siem roles with and feature_foo and feature_bar privileges', async () => { + mockAsCurrentUser.asCurrentUser.security.getRole.mockResolvedValue( + elasticsearchServiceMock.createSuccessTransportRequestPromise({ + first_role: { + cluster: [], + indices: [], + applications: [ + { + application, + privileges: [ + 'feature_foo.foo-privilege-1', + 'feature_foo.foo-privilege-2', + 'feature_bar.bar-privilege-1', + ], + resources: ['space:securitySolutions'], + }, + ], + run_as: [], + metadata: { + _reserved: true, + }, + transient_metadata: { + enabled: true, + }, + }, + }) + ); + + const mockContext = { + esClient: mockAsCurrentUser, + savedObjectsClient: jest.fn(), + } as unknown as GetDeprecationsContext; + + const resp = await getKibanaRolesByFeatureId({ context: mockContext, featureId: 'siem' }); + expect(resp).toMatchInlineSnapshot(` + Object { + "roles": Array [], + } + `); + }); + + it('unhappy path with status code 400, we should have the attribute errors', async () => { + mockAsCurrentUser.asCurrentUser.security.getRole.mockResolvedValue( + elasticsearchServiceMock.createErrorTransportRequestPromise({ + message: 'Test error', + statusCode: 400, + }) + ); + + const mockContext = { + esClient: mockAsCurrentUser, + savedObjectsClient: jest.fn(), + } as unknown as GetDeprecationsContext; + + const resp = await getKibanaRolesByFeatureId({ context: mockContext, featureId: 'siem' }); + expect(resp).toMatchInlineSnapshot(` + Object { + "errors": Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "A user with the \\"manage_security\\" cluster privilege is required to perform this check.", + ], + }, + "level": "fetch_error", + "message": "Error retrieving roles for privilege deprecations: Test error", + "title": "Error in privilege deprecations services", + }, + ], + } + `); + }); + + it('unhappy path with status code 403, we should have unauthorized message in the attribute errors', async () => { + mockAsCurrentUser.asCurrentUser.security.getRole.mockResolvedValue( + elasticsearchServiceMock.createErrorTransportRequestPromise({ + message: 'Test error', + statusCode: 403, + }) + ); + + const mockContext = { + esClient: mockAsCurrentUser, + savedObjectsClient: jest.fn(), + } as unknown as GetDeprecationsContext; + + const resp = await getKibanaRolesByFeatureId({ context: mockContext, featureId: 'siem' }); + expect(resp).toMatchInlineSnapshot(` + Object { + "errors": Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "A user with the \\"manage_security\\" cluster privilege is required to perform this check.", + ], + }, + "level": "fetch_error", + "message": "You must have the 'manage_security' cluster privilege to fix role deprecations.", + "title": "Error in privilege deprecations services", + }, + ], + } + `); + }); + }); +}); diff --git a/x-pack/plugins/security/server/deprecations/privilege_deprecations.ts b/x-pack/plugins/security/server/deprecations/privilege_deprecations.ts new file mode 100644 index 0000000000000..df212d5c7bde3 --- /dev/null +++ b/x-pack/plugins/security/server/deprecations/privilege_deprecations.ts @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import type { Logger } from 'src/core/server'; + +import type { SecurityLicense } from '../../common/licensing'; +import type { + PrivilegeDeprecationsRolesByFeatureIdRequest, + PrivilegeDeprecationsRolesByFeatureIdResponse, +} from '../../common/model'; +import { transformElasticsearchRoleToRole } from '../authorization'; +import type { AuthorizationServiceSetupInternal, ElasticsearchRole } from '../authorization'; +import { getDetailedErrorMessage, getErrorStatusCode } from '../errors'; + +export const getPrivilegeDeprecationsService = ( + authz: Pick<AuthorizationServiceSetupInternal, 'applicationName'>, + license: SecurityLicense, + logger: Logger +) => { + const getKibanaRolesByFeatureId = async ({ + context, + featureId, + }: PrivilegeDeprecationsRolesByFeatureIdRequest): Promise<PrivilegeDeprecationsRolesByFeatureIdResponse> => { + // Nothing to do if security is disabled + if (!license.isEnabled()) { + return { + roles: [], + }; + } + let kibanaRoles; + try { + const { body: elasticsearchRoles } = await context.esClient.asCurrentUser.security.getRole< + Record<string, ElasticsearchRole> + >(); + kibanaRoles = Object.entries(elasticsearchRoles).map(([roleName, elasticsearchRole]) => + transformElasticsearchRoleToRole( + // @ts-expect-error `SecurityIndicesPrivileges.names` expected to be `string[]` + elasticsearchRole, + roleName, + authz.applicationName + ) + ); + } catch (e) { + const statusCode = getErrorStatusCode(e); + const isUnauthorized = statusCode === 403; + const message = isUnauthorized + ? i18n.translate('xpack.security.privilegeDeprecationsService.error.unauthorized.message', { + defaultMessage: `You must have the 'manage_security' cluster privilege to fix role deprecations.`, + }) + : i18n.translate( + 'xpack.security.privilegeDeprecationsService.error.retrievingRoles.message', + { + defaultMessage: `Error retrieving roles for privilege deprecations: {message}`, + values: { + message: getDetailedErrorMessage(e), + }, + } + ); + + if (isUnauthorized) { + logger.warn( + `Failed to retrieve roles when checking for deprecations: the manage_security cluster privilege is required` + ); + } else { + logger.error( + `Failed to retrieve roles when checking for deprecations, unexpected error: ${getDetailedErrorMessage( + e + )}` + ); + } + + return { + errors: [ + { + title: i18n.translate('xpack.security.privilegeDeprecationsService.error.title', { + defaultMessage: `Error in privilege deprecations services`, + }), + level: 'fetch_error', + message, + correctiveActions: { + manualSteps: [ + i18n.translate('xpack.security.privilegeDeprecationsService.manualSteps.message', { + defaultMessage: + 'A user with the "manage_security" cluster privilege is required to perform this check.', + }), + ], + }, + }, + ], + }; + } + return { + roles: kibanaRoles.filter((role) => + role.kibana.find((privilege) => Object.hasOwnProperty.call(privilege.feature, featureId)) + ), + }; + }; + return Object.freeze({ + getKibanaRolesByFeatureId, + }); +}; diff --git a/x-pack/plugins/security/server/mocks.ts b/x-pack/plugins/security/server/mocks.ts index f1f858a40a465..7cae0d29bf943 100644 --- a/x-pack/plugins/security/server/mocks.ts +++ b/x-pack/plugins/security/server/mocks.ts @@ -28,6 +28,9 @@ function createSetupMock() { }, registerSpacesService: jest.fn(), license: licenseMock.create(), + privilegeDeprecationsService: { + getKibanaRolesByFeatureId: jest.fn(), + }, }; } diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index eb88aba1c0e1b..4784e14a11fb4 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -123,6 +123,9 @@ describe('Security Plugin', () => { "isEnabled": [Function], "isLicenseAvailable": [Function], }, + "privilegeDeprecationsService": Object { + "getKibanaRolesByFeatureId": [Function], + }, } `); }); diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index e3da0716f29ee..2ad75a1c53174 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -30,7 +30,7 @@ import type { SpacesPluginSetup, SpacesPluginStart } from '../../spaces/server'; import type { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; import type { SecurityLicense } from '../common/licensing'; import { SecurityLicenseService } from '../common/licensing'; -import type { AuthenticatedUser } from '../common/model'; +import type { AuthenticatedUser, PrivilegeDeprecationsService } from '../common/model'; import type { AnonymousAccessServiceStart } from './anonymous_access'; import { AnonymousAccessService } from './anonymous_access'; import type { AuditServiceSetup } from './audit'; @@ -44,6 +44,7 @@ import type { AuthorizationServiceSetup, AuthorizationServiceSetupInternal } fro import { AuthorizationService } from './authorization'; import type { ConfigSchema, ConfigType } from './config'; import { createConfig } from './config'; +import { getPrivilegeDeprecationsService } from './deprecations'; import { ElasticsearchService } from './elasticsearch'; import type { SecurityFeatureUsageServiceStart } from './feature_usage'; import { SecurityFeatureUsageService } from './feature_usage'; @@ -85,6 +86,10 @@ export interface SecurityPluginSetup { * Exposes services for audit logging. */ audit: AuditServiceSetup; + /** + * Exposes services to access kibana roles per feature id with the GetDeprecationsContext + */ + privilegeDeprecationsService: PrivilegeDeprecationsService; } /** @@ -321,9 +326,7 @@ export class SecurityPlugin asScoped: this.auditSetup.asScoped, getLogger: this.auditSetup.getLogger, }, - authc: { getCurrentUser: (request) => this.getAuthentication().getCurrentUser(request) }, - authz: { actions: this.authorizationSetup.actions, checkPrivilegesWithRequest: this.authorizationSetup.checkPrivilegesWithRequest, @@ -333,8 +336,12 @@ export class SecurityPlugin this.authorizationSetup.checkSavedObjectsPrivilegesWithRequest, mode: this.authorizationSetup.mode, }, - license, + privilegeDeprecationsService: getPrivilegeDeprecationsService( + this.authorizationSetup, + license, + this.logger.get('deprecations') + ), }); } diff --git a/x-pack/plugins/security/server/routes/authorization/roles/model/index.ts b/x-pack/plugins/security/server/routes/authorization/roles/model/index.ts index 8334dd3c05476..e090cd26dc39f 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/model/index.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/model/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -export { ElasticsearchRole, transformElasticsearchRoleToRole } from './elasticsearch_role'; +export { ElasticsearchRole, transformElasticsearchRoleToRole } from '../../../../authorization'; export { getPutPayloadSchema, transformPutPayloadToElasticsearchRole } from './put_payload'; diff --git a/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.ts b/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.ts index 8a560d7b6dd87..7fb2baf0fd410 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/model/put_payload.ts @@ -10,10 +10,10 @@ import _ from 'lodash'; import type { TypeOf } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema'; +import type { ElasticsearchRole } from '.'; import { GLOBAL_RESOURCE } from '../../../../../common/constants'; import { PrivilegeSerializer } from '../../../../authorization/privilege_serializer'; import { ResourceSerializer } from '../../../../authorization/resource_serializer'; -import type { ElasticsearchRole } from './elasticsearch_role'; /** * Elasticsearch specific portion of the role definition. diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index a1b8ca98afc20..1492e0e8c82c9 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -1663,6 +1663,7 @@ export class EndpointDocGenerator extends BaseDataGenerator { install_status: 'installed', install_started_at: '2020-06-24T14:41:23.098Z', install_source: 'registry', + keep_policies_up_to_date: false, }, references: [], updated_at: '2020-06-24T14:41:23.098Z', diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index db76bfc3cf4df..788e177fec721 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -72,10 +72,6 @@ export interface OverrideRule extends CustomRule { timestampOverride: string; } -export interface EventCorrelationRule extends CustomRule { - language: string; -} - export interface ThreatIndicatorRule extends CustomRule { indicatorIndexPattern: string[]; indicatorMappingField: string; @@ -330,7 +326,7 @@ export const getEqlRule = (): CustomRule => ({ maxSignals: 100, }); -export const getCCSEqlRule = (): EventCorrelationRule => ({ +export const getCCSEqlRule = (): CustomRule => ({ customQuery: 'any where process.name == "run-parts"', name: 'New EQL Rule', index: [`${ccsRemoteName}:run-parts`], @@ -346,7 +342,6 @@ export const getCCSEqlRule = (): EventCorrelationRule => ({ lookBack: getLookBack(), timeline: getTimeline(), maxSignals: 100, - language: 'eql', }); export const getEqlSequenceRule = (): CustomRule => ({ diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts index 130467cde053d..04ff0fcabc081 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CustomRule, EventCorrelationRule, ThreatIndicatorRule } from '../../objects/rule'; +import { CustomRule, ThreatIndicatorRule } from '../../objects/rule'; export const createCustomRule = (rule: CustomRule, ruleId = 'rule_testing', interval = '100m') => cy.request({ @@ -29,7 +29,7 @@ export const createCustomRule = (rule: CustomRule, ruleId = 'rule_testing', inte failOnStatusCode: false, }); -export const createEventCorrelationRule = (rule: EventCorrelationRule, ruleId = 'rule_testing') => +export const createEventCorrelationRule = (rule: CustomRule, ruleId = 'rule_testing') => cy.request({ method: 'POST', url: 'api/detection_engine/rules', diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts index ed5dbbd09d79a..71df9902223da 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.test.ts @@ -151,6 +151,7 @@ describe('Host Isolation', () => { type: ElasticsearchAssetType.transform, }, ], + keep_policies_up_to_date: false, }) ); licenseEmitter = new Subject(); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts index 3fa90ad6d27a5..d9016e7a9c7cb 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts @@ -131,6 +131,7 @@ describe('test endpoint route', () => { type: ElasticsearchAssetType.transform, }, ], + keep_policies_up_to_date: false, }) ); endpointAppContextService.start({ ...startContract, packageService: mockPackageService }); @@ -390,6 +391,7 @@ describe('test endpoint route', () => { type: ElasticsearchAssetType.transform, }, ], + keep_policies_up_to_date: false, }) ); endpointAppContextService.start({ ...startContract, packageService: mockPackageService }); diff --git a/x-pack/plugins/spaces/public/mocks.ts b/x-pack/plugins/spaces/public/mocks.ts index 897f58e1d649c..76cafd4c7f5ae 100644 --- a/x-pack/plugins/spaces/public/mocks.ts +++ b/x-pack/plugins/spaces/public/mocks.ts @@ -41,6 +41,7 @@ const createApiUiComponentsMock = () => { getSpaceList: jest.fn(), getLegacyUrlConflict: jest.fn(), getSpaceAvatar: jest.fn(), + getSavedObjectConflictMessage: jest.fn(), }; return mock; diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/get_saved_object_conflict_message.tsx b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/get_saved_object_conflict_message.tsx new file mode 100644 index 0000000000000..66b2a5652057a --- /dev/null +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/get_saved_object_conflict_message.tsx @@ -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 React from 'react'; + +import type { SavedObjectConflictMessageProps } from '../types'; + +export const getSavedObjectConflictMessage = async (): Promise< + React.FC<SavedObjectConflictMessageProps> +> => { + const { SavedObjectConflictMessage } = await import('./saved_object_conflict_message'); + return (props: SavedObjectConflictMessageProps) => { + return <SavedObjectConflictMessage {...props} />; + }; +}; diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/index.ts b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/index.ts index c0828e3b5331d..fa641d03fd715 100644 --- a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/index.ts +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/index.ts @@ -6,4 +6,5 @@ */ export { getShareToSpaceFlyoutComponent } from './share_to_space_flyout'; +export { getSavedObjectConflictMessage } from './get_saved_object_conflict_message'; export { getLegacyUrlConflict } from './legacy_url_conflict'; diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/saved_object_conflict_message.tsx b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/saved_object_conflict_message.tsx new file mode 100644 index 0000000000000..22a1ad7cd20aa --- /dev/null +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/saved_object_conflict_message.tsx @@ -0,0 +1,56 @@ +/* + * 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 { EuiButtonEmpty, EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; +import React, { useState } from 'react'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import type { SavedObjectConflictMessageProps } from '../types'; + +export const SavedObjectConflictMessage = ({ json }: SavedObjectConflictMessageProps) => { + const [expandError, setExpandError] = useState(false); + return ( + <> + <FormattedMessage + id="xpack.spaces.legacyURLConflict.longMessage" + defaultMessage="Disable the {documentationLink} associated with this object." + values={{ + documentationLink: ( + <EuiLink + external + href="https://www.elastic.co/guide/en/kibana/master/legacy-url-aliases.html" + target="_blank" + > + {i18n.translate('xpack.spaces.legacyURLConflict.documentationLinkText', { + defaultMessage: 'legacy URL alias', + })} + </EuiLink> + ), + }} + /> + <EuiSpacer /> + {expandError ? ( + <EuiCallOut + title={i18n.translate('xpack.spaces.legacyURLConflict.expandErrorText', { + defaultMessage: `This object has the same URL as a legacy alias. Disable the alias to resolve this error : {json}`, + values: { json }, + })} + color="danger" + iconType="alert" + /> + ) : ( + <EuiButtonEmpty onClick={() => setExpandError(true)}> + {i18n.translate('xpack.spaces.legacyURLConflict.expandError', { + defaultMessage: `Show more`, + })} + </EuiButtonEmpty> + )} + </> + ); +}; diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/index.ts b/x-pack/plugins/spaces/public/share_saved_objects_to_space/index.ts index fe90ee8d6a8a9..465fd179c8441 100644 --- a/x-pack/plugins/spaces/public/share_saved_objects_to_space/index.ts +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/index.ts @@ -5,10 +5,15 @@ * 2.0. */ -export { getShareToSpaceFlyoutComponent, getLegacyUrlConflict } from './components'; +export { + getShareToSpaceFlyoutComponent, + getLegacyUrlConflict, + getSavedObjectConflictMessage, +} from './components'; export { createRedirectLegacyUrl } from './utils'; export type { LegacyUrlConflictProps, ShareToSpaceFlyoutProps, ShareToSpaceSavedObjectTarget, + SavedObjectConflictMessageProps, } from './types'; diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/types.ts b/x-pack/plugins/spaces/public/share_saved_objects_to_space/types.ts index 1beccaa546282..21290f2b90de5 100644 --- a/x-pack/plugins/spaces/public/share_saved_objects_to_space/types.ts +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/types.ts @@ -140,3 +140,10 @@ export interface ShareToSpaceSavedObjectTarget { */ noun?: string; } + +/** + * Properties for the SavedObjectConflictMessage component. + */ +export interface SavedObjectConflictMessageProps { + json: string; +} diff --git a/x-pack/plugins/spaces/public/ui_api/components.tsx b/x-pack/plugins/spaces/public/ui_api/components.tsx index a33480712ffae..71dc2e34a0d70 100644 --- a/x-pack/plugins/spaces/public/ui_api/components.tsx +++ b/x-pack/plugins/spaces/public/ui_api/components.tsx @@ -14,6 +14,7 @@ import { getCopyToSpaceFlyoutComponent } from '../copy_saved_objects_to_space'; import type { PluginsStart } from '../plugin'; import { getLegacyUrlConflict, + getSavedObjectConflictMessage, getShareToSpaceFlyoutComponent, } from '../share_saved_objects_to_space'; import { getSpaceAvatarComponent } from '../space_avatar'; @@ -56,5 +57,6 @@ export const getComponents = ({ getSpaceList: wrapLazy(getSpaceListComponent), getLegacyUrlConflict: wrapLazy(() => getLegacyUrlConflict({ getStartServices })), getSpaceAvatar: wrapLazy(getSpaceAvatarComponent), + getSavedObjectConflictMessage: wrapLazy(() => getSavedObjectConflictMessage()), }; }; diff --git a/x-pack/plugins/spaces/public/ui_api/types.ts b/x-pack/plugins/spaces/public/ui_api/types.ts index 5048e5a9b9652..67e43f0cd31a6 100644 --- a/x-pack/plugins/spaces/public/ui_api/types.ts +++ b/x-pack/plugins/spaces/public/ui_api/types.ts @@ -12,6 +12,7 @@ import type { CoreStart } from 'src/core/public'; import type { CopyToSpaceFlyoutProps } from '../copy_saved_objects_to_space'; import type { LegacyUrlConflictProps, + SavedObjectConflictMessageProps, ShareToSpaceFlyoutProps, } from '../share_saved_objects_to_space'; import type { SpaceAvatarProps } from '../space_avatar'; @@ -109,4 +110,8 @@ export interface SpacesApiUiComponent { * Displays an avatar for the given space. */ getSpaceAvatar: LazyComponentFn<SpaceAvatarProps>; + /** + * Displays a saved object conflict message that directs user to disable legacy URL alias + */ + getSavedObjectConflictMessage: LazyComponentFn<SavedObjectConflictMessageProps>; } diff --git a/x-pack/plugins/stack_alerts/common/config.ts b/x-pack/plugins/stack_alerts/common/config.ts index ebc12ee563350..e1eb28f092408 100644 --- a/x-pack/plugins/stack_alerts/common/config.ts +++ b/x-pack/plugins/stack_alerts/common/config.ts @@ -7,8 +7,6 @@ import { schema, TypeOf } from '@kbn/config-schema'; -export const configSchema = schema.object({ - enabled: schema.boolean({ defaultValue: true }), -}); +export const configSchema = schema.object({}); export type Config = TypeOf<typeof configSchema>; diff --git a/x-pack/plugins/task_manager/server/config.test.ts b/x-pack/plugins/task_manager/server/config.test.ts index e9701fc3e7c05..8d7a6c7872e7e 100644 --- a/x-pack/plugins/task_manager/server/config.test.ts +++ b/x-pack/plugins/task_manager/server/config.test.ts @@ -12,7 +12,6 @@ describe('config validation', () => { const config: Record<string, unknown> = {}; expect(configSchema.validate(config)).toMatchInlineSnapshot(` Object { - "enabled": true, "ephemeral_tasks": Object { "enabled": false, "request_capacity": 10, @@ -71,7 +70,6 @@ describe('config validation', () => { const config: Record<string, unknown> = {}; expect(configSchema.validate(config)).toMatchInlineSnapshot(` Object { - "enabled": true, "ephemeral_tasks": Object { "enabled": false, "request_capacity": 10, @@ -117,7 +115,6 @@ describe('config validation', () => { }; expect(configSchema.validate(config)).toMatchInlineSnapshot(` Object { - "enabled": true, "ephemeral_tasks": Object { "enabled": false, "request_capacity": 10, diff --git a/x-pack/plugins/task_manager/server/config.ts b/x-pack/plugins/task_manager/server/config.ts index 286a9feaa1b5e..f2026ecac3adc 100644 --- a/x-pack/plugins/task_manager/server/config.ts +++ b/x-pack/plugins/task_manager/server/config.ts @@ -43,7 +43,6 @@ export const taskExecutionFailureThresholdSchema = schema.object( export const configSchema = schema.object( { - enabled: schema.boolean({ defaultValue: true }), /* The maximum number of times a task will be attempted before being abandoned as failed */ max_attempts: schema.number({ defaultValue: 3, diff --git a/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts b/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts index 558aa108c2462..ec6f25b7f1b61 100644 --- a/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts +++ b/x-pack/plugins/task_manager/server/ephemeral_task_lifecycle.test.ts @@ -42,7 +42,6 @@ describe('EphemeralTaskLifecycle', () => { definitions: new TaskTypeDictionary(taskManagerLogger), executionContext, config: { - enabled: true, max_workers: 10, index: 'foo', max_attempts: 9, diff --git a/x-pack/plugins/task_manager/server/index.test.ts b/x-pack/plugins/task_manager/server/index.test.ts index 8419e826dfd31..8d095ce8131cd 100644 --- a/x-pack/plugins/task_manager/server/index.test.ts +++ b/x-pack/plugins/task_manager/server/index.test.ts @@ -52,13 +52,4 @@ describe('deprecations', () => { ] `); }); - - it('logs a deprecation warning for the enabled config', () => { - const { messages } = applyTaskManagerDeprecations({ enabled: true }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "\\"xpack.task_manager.enabled\\" is deprecated. The ability to disable this plugin will be removed in 8.0.0.", - ] - `); - }); }); diff --git a/x-pack/plugins/task_manager/server/index.ts b/x-pack/plugins/task_manager/server/index.ts index 368b5a3441778..611fa40591c4d 100644 --- a/x-pack/plugins/task_manager/server/index.ts +++ b/x-pack/plugins/task_manager/server/index.ts @@ -68,16 +68,5 @@ export const config: PluginConfigDescriptor<TaskManagerConfig> = { }); } }, - (settings, fromPath, addDeprecation) => { - const taskManager = get(settings, fromPath); - if (taskManager?.enabled === false || taskManager?.enabled === true) { - addDeprecation({ - message: `"xpack.task_manager.enabled" is deprecated. The ability to disable this plugin will be removed in 8.0.0.`, - correctiveActions: { - manualSteps: [`Remove "xpack.task_manager.enabled" from your kibana configs.`], - }, - }); - } - }, ], }; diff --git a/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts b/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts index f714fd36c2658..c9cc5be2d5cd6 100644 --- a/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts +++ b/x-pack/plugins/task_manager/server/integration_tests/managed_configuration.test.ts @@ -29,7 +29,6 @@ describe('managed configuration', () => { clock = sinon.useFakeTimers(); const context = coreMock.createPluginInitializerContext<TaskManagerConfig>({ - enabled: true, max_workers: 10, index: 'foo', max_attempts: 9, diff --git a/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts index 6e88e9803add2..bbd5bc217ae3b 100644 --- a/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts @@ -13,7 +13,6 @@ import { TaskManagerConfig } from '../config'; describe('Configuration Statistics Aggregator', () => { test('merges the static config with the merged configs', async () => { const configuration: TaskManagerConfig = { - enabled: true, max_workers: 10, index: 'foo', max_attempts: 9, diff --git a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts index ec94d9df1a4dc..e29dbc978c64a 100644 --- a/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/monitoring_stats_stream.test.ts @@ -17,7 +17,6 @@ beforeEach(() => { describe('createMonitoringStatsStream', () => { const configuration: TaskManagerConfig = { - enabled: true, max_workers: 10, index: 'foo', max_attempts: 9, diff --git a/x-pack/plugins/task_manager/server/plugin.test.ts b/x-pack/plugins/task_manager/server/plugin.test.ts index c47f006eca415..c2345d7bf8193 100644 --- a/x-pack/plugins/task_manager/server/plugin.test.ts +++ b/x-pack/plugins/task_manager/server/plugin.test.ts @@ -16,7 +16,6 @@ describe('TaskManagerPlugin', () => { describe('setup', () => { test('throws if no valid UUID is available', async () => { const pluginInitializerContext = coreMock.createPluginInitializerContext<TaskManagerConfig>({ - enabled: true, max_workers: 10, index: 'foo', max_attempts: 9, @@ -59,7 +58,6 @@ describe('TaskManagerPlugin', () => { test('throws if setup methods are called after start', async () => { const pluginInitializerContext = coreMock.createPluginInitializerContext<TaskManagerConfig>({ - enabled: true, max_workers: 10, index: 'foo', max_attempts: 9, @@ -131,7 +129,6 @@ describe('TaskManagerPlugin', () => { test('it logs a warning when the unsafe `exclude_task_types` config is used', async () => { const pluginInitializerContext = coreMock.createPluginInitializerContext<TaskManagerConfig>({ - enabled: true, max_workers: 10, index: 'foo', max_attempts: 9, diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 475c8b9645007..df680d77e72df 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1653,12 +1653,6 @@ "data.functions.esaggs.help": "AggConfig 集約を実行します", "data.functions.esaggs.inspector.dataRequest.description": "このリクエストはElasticsearchにクエリし、ビジュアライゼーション用のデータを取得します。", "data.functions.esaggs.inspector.dataRequest.title": "データ", - "dataViews.indexPatternLoad.help": "インデックスパターンを読み込みます", - "dataViews.functions.indexPatternLoad.id.help": "読み込むインデックスパターンID", - "dataViews.ensureDefaultIndexPattern.bannerLabel": "Kibanaでデータの可視化と閲覧を行うには、Elasticsearchからデータを取得するためのインデックスパターンの作成が必要です。", - "dataViews.fetchFieldErrorTitle": "インデックスパターンのフィールド取得中にエラーが発生 {title}(ID:{id})", - "dataViews.indexPatternLoad.error.kibanaRequest": "サーバーでこの検索を実行するには、KibanaRequest が必要です。式実行パラメーターに要求オブジェクトを渡してください。", - "dataViews.unableWriteLabel": "インデックスパターンを書き込めません。このインデックスパターンへの最新の変更を取得するには、ページを更新してください。", "data.inspector.table..dataDescriptionTooltip": "ビジュアライゼーションの元のデータを表示", "data.inspector.table.dataTitle": "データ", "data.inspector.table.downloadCSVToggleButtonLabel": "CSV をダウンロード", @@ -2297,6 +2291,12 @@ "data.searchSessions.sessionService.sessionObjectFetchError": "検索セッション情報を取得できませんでした", "data.triggers.applyFilterDescription": "Kibanaフィルターが適用されるとき。単一の値または範囲フィルターにすることができます。", "data.triggers.applyFilterTitle": "フィルターを適用", + "dataViews.indexPatternLoad.help": "インデックスパターンを読み込みます", + "dataViews.functions.indexPatternLoad.id.help": "読み込むインデックスパターンID", + "dataViews.ensureDefaultIndexPattern.bannerLabel": "Kibanaでデータの可視化と閲覧を行うには、Elasticsearchからデータを取得するためのインデックスパターンの作成が必要です。", + "dataViews.fetchFieldErrorTitle": "インデックスパターンのフィールド取得中にエラーが発生 {title}(ID:{id})", + "dataViews.indexPatternLoad.error.kibanaRequest": "サーバーでこの検索を実行するには、KibanaRequest が必要です。式実行パラメーターに要求オブジェクトを渡してください。", + "dataViews.unableWriteLabel": "インデックスパターンを書き込めません。このインデックスパターンへの最新の変更を取得するには、ページを更新してください。", "devTools.badge.readOnly.text": "読み取り専用", "devTools.badge.readOnly.tooltip": "を保存できませんでした", "devTools.devToolsTitle": "開発ツール", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ed68a24fb74d5..131704958361a 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1669,12 +1669,6 @@ "data.functions.esaggs.help": "运行 AggConfig 聚合", "data.functions.esaggs.inspector.dataRequest.description": "此请求查询 Elasticsearch,以获取可视化的数据。", "data.functions.esaggs.inspector.dataRequest.title": "数据", - "dataViews.indexPatternLoad.help": "加载索引模式", - "dataViews.functions.indexPatternLoad.id.help": "要加载的索引模式 id", - "dataViews.ensureDefaultIndexPattern.bannerLabel": "要在 Kibana 中可视化和浏览数据,必须创建索引模式,以从 Elasticsearch 中检索数据。", - "dataViews.fetchFieldErrorTitle": "提取索引模式 {title} (ID: {id}) 的字段时出错", - "dataViews.indexPatternLoad.error.kibanaRequest": "在服务器上执行此搜索时需要 Kibana 请求。请向表达式执行模式参数提供请求对象。", - "dataViews.unableWriteLabel": "无法写入索引模式!请刷新页面以获取此索引模式的最新更改。", "data.inspector.table..dataDescriptionTooltip": "查看可视化后面的数据", "data.inspector.table.dataTitle": "数据", "data.inspector.table.downloadCSVToggleButtonLabel": "下载 CSV", @@ -2319,6 +2313,12 @@ "data.searchSessions.sessionService.sessionObjectFetchError": "无法提取搜索会话信息", "data.triggers.applyFilterDescription": "应用 kibana 筛选时。可能是单个值或范围筛选。", "data.triggers.applyFilterTitle": "应用筛选", + "dataViews.indexPatternLoad.help": "加载索引模式", + "dataViews.functions.indexPatternLoad.id.help": "要加载的索引模式 id", + "dataViews.ensureDefaultIndexPattern.bannerLabel": "要在 Kibana 中可视化和浏览数据,必须创建索引模式,以从 Elasticsearch 中检索数据。", + "dataViews.fetchFieldErrorTitle": "提取索引模式 {title} (ID: {id}) 的字段时出错", + "dataViews.indexPatternLoad.error.kibanaRequest": "在服务器上执行此搜索时需要 Kibana 请求。请向表达式执行模式参数提供请求对象。", + "dataViews.unableWriteLabel": "无法写入索引模式!请刷新页面以获取此索引模式的最新更改。", "devTools.badge.readOnly.text": "只读", "devTools.badge.readOnly.tooltip": "无法保存", "devTools.devToolsTitle": "开发工具", diff --git a/x-pack/plugins/triggers_actions_ui/server/index.test.ts b/x-pack/plugins/triggers_actions_ui/server/index.test.ts deleted file mode 100644 index 1149843d85a50..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/server/index.test.ts +++ /dev/null @@ -1,44 +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 { config } from './index'; -import { applyDeprecations, configDeprecationFactory } from '@kbn/config'; - -const CONFIG_PATH = 'xpack.trigger_actions_ui'; -const applyStackAlertDeprecations = (settings: Record<string, unknown> = {}) => { - const deprecations = config.deprecations!(configDeprecationFactory); - const deprecationMessages: string[] = []; - const _config = { - [CONFIG_PATH]: settings, - }; - const { config: migrated } = applyDeprecations( - _config, - deprecations.map((deprecation) => ({ - deprecation, - path: CONFIG_PATH, - })), - () => - ({ message }) => - deprecationMessages.push(message) - ); - return { - messages: deprecationMessages, - migrated, - }; -}; - -describe('index', () => { - describe('deprecations', () => { - it('should deprecate .enabled flag', () => { - const { messages } = applyStackAlertDeprecations({ enabled: false }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "\\"xpack.trigger_actions_ui.enabled\\" is deprecated. The ability to disable this plugin will be removed in 8.0.0.", - ] - `); - }); - }); -}); diff --git a/x-pack/plugins/triggers_actions_ui/server/index.ts b/x-pack/plugins/triggers_actions_ui/server/index.ts index c7d363af45247..89c17ea0d4189 100644 --- a/x-pack/plugins/triggers_actions_ui/server/index.ts +++ b/x-pack/plugins/triggers_actions_ui/server/index.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { get } from 'lodash'; import { PluginConfigDescriptor, PluginInitializerContext } from 'kibana/server'; import { configSchema, ConfigSchema } from '../config'; import { TriggersActionsPlugin } from './plugin'; @@ -26,19 +25,6 @@ export const config: PluginConfigDescriptor<ConfigSchema> = { enableGeoTrackingThresholdAlert: true, }, schema: configSchema, - deprecations: () => [ - (settings, fromPath, addDeprecation) => { - const triggersActionsUi = get(settings, fromPath); - if (triggersActionsUi?.enabled === false || triggersActionsUi?.enabled === true) { - addDeprecation({ - message: `"xpack.trigger_actions_ui.enabled" is deprecated. The ability to disable this plugin will be removed in 8.0.0.`, - correctiveActions: { - manualSteps: [`Remove "xpack.trigger_actions_ui.enabled" from your kibana configs.`], - }, - }); - } - }, - ], }; export const plugin = (ctx: PluginInitializerContext) => new TriggersActionsPlugin(ctx); diff --git a/x-pack/test/accessibility/apps/ingest_node_pipelines.ts b/x-pack/test/accessibility/apps/ingest_node_pipelines.ts index b9bc216db60b5..dab9c86bf018e 100644 --- a/x-pack/test/accessibility/apps/ingest_node_pipelines.ts +++ b/x-pack/test/accessibility/apps/ingest_node_pipelines.ts @@ -14,14 +14,14 @@ export default function ({ getService, getPageObjects }: any) { const log = getService('log'); const a11y = getService('a11y'); /* this is the wrapping service around axe */ - describe('Ingest Node Pipelines', async () => { + describe('Ingest Pipelines', async () => { before(async () => { await putSamplePipeline(esClient); await common.navigateToApp('ingestPipelines'); }); it('List View', async () => { - await retry.waitFor('Ingest Node Pipelines page to be visible', async () => { + await retry.waitFor('Ingest Pipelines page to be visible', async () => { await common.navigateToApp('ingestPipelines'); return testSubjects.exists('pipelineDetailsLink') ? true : false; }); diff --git a/x-pack/test/accessibility/apps/login_page.ts b/x-pack/test/accessibility/apps/login_page.ts index 580df3e4ccc88..8de4a47e10b1e 100644 --- a/x-pack/test/accessibility/apps/login_page.ts +++ b/x-pack/test/accessibility/apps/login_page.ts @@ -14,7 +14,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'security']); - describe('Security', () => { + // Failing: See https://github.com/elastic/kibana/issues/96372 + describe.skip('Security', () => { describe('Login Page', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); diff --git a/x-pack/test/api_integration/apis/management/ingest_pipelines/index.ts b/x-pack/test/api_integration/apis/management/ingest_pipelines/index.ts index 54bd29ede5865..d36c573e14e1f 100644 --- a/x-pack/test/api_integration/apis/management/ingest_pipelines/index.ts +++ b/x-pack/test/api_integration/apis/management/ingest_pipelines/index.ts @@ -8,7 +8,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { - describe('Ingest Node Pipelines', () => { + describe('Ingest Pipelines', () => { loadTestFile(require.resolve('./ingest_pipelines')); }); } diff --git a/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts b/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts index 1ad2a05d4f783..80790a6df400f 100644 --- a/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts +++ b/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts @@ -145,7 +145,7 @@ export default function ({ getService }: FtrProviderContext) { await createPipeline({ body: PIPELINE, id: PIPELINE_ID }, true); } catch (err) { // eslint-disable-next-line no-console - console.log('[Setup error] Error creating ingest node pipeline'); + console.log('[Setup error] Error creating ingest pipeline'); throw err; } }); @@ -225,7 +225,7 @@ export default function ({ getService }: FtrProviderContext) { await createPipeline({ body: PIPELINE, id: PIPELINE_ID }, true); } catch (err) { // eslint-disable-next-line no-console - console.log('[Setup error] Error creating ingest node pipeline'); + console.log('[Setup error] Error creating ingest pipeline'); throw err; } }); diff --git a/x-pack/test/banners_functional/config.ts b/x-pack/test/banners_functional/config.ts index c9acff91aecd1..03f91dfbc34e2 100644 --- a/x-pack/test/banners_functional/config.ts +++ b/x-pack/test/banners_functional/config.ts @@ -32,7 +32,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...kibanaFunctionalConfig.get('kbnTestServer'), serverArgs: [ ...kibanaFunctionalConfig.get('kbnTestServer.serverArgs'), - '--xpack.banners.placement=header', + '--xpack.banners.placement=top', '--xpack.banners.textContent="global banner text"', ], }, diff --git a/x-pack/test/case_api_integration/common/config.ts b/x-pack/test/case_api_integration/common/config.ts index 514b54982ee42..e6fda129eaa16 100644 --- a/x-pack/test/case_api_integration/common/config.ts +++ b/x-pack/test/case_api_integration/common/config.ts @@ -140,7 +140,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) (pluginDir) => `--plugin-path=${path.resolve(__dirname, 'fixtures', 'plugins', pluginDir)}` ), - `--server.xsrf.whitelist=${JSON.stringify(getAllExternalServiceSimulatorPaths())}`, + `--server.xsrf.allowlist=${JSON.stringify(getAllExternalServiceSimulatorPaths())}`, ...(ssl ? [ `--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`, diff --git a/x-pack/test/fleet_api_integration/apis/epm/__snapshots__/install_by_upload.snap b/x-pack/test/fleet_api_integration/apis/epm/__snapshots__/install_by_upload.snap index 8f9428d8a12db..8e06e62385315 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/__snapshots__/install_by_upload.snap +++ b/x-pack/test/fleet_api_integration/apis/epm/__snapshots__/install_by_upload.snap @@ -276,6 +276,7 @@ Object { "type": "image/svg+xml", }, ], + "keepPoliciesUpToDate": false, "license": "basic", "name": "apache", "owner": Object { @@ -449,6 +450,7 @@ Object { }, ], "internal": false, + "keep_policies_up_to_date": false, "name": "apache", "package_assets": Array [ Object { diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts index 348b4bef59b30..e57899531e939 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts @@ -618,6 +618,7 @@ const expectAssetsInstalled = ({ install_status: 'installed', install_started_at: res.attributes.install_started_at, install_source: 'registry', + keep_policies_up_to_date: false, }); }); }; diff --git a/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts index 390be9bf6ea19..3516eccf9bb15 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts @@ -432,6 +432,7 @@ export default function (providerContext: FtrProviderContext) { install_status: 'installed', install_started_at: res.attributes.install_started_at, install_source: 'registry', + keep_policies_up_to_date: false, }); }); }); diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.2.5-non-breaking-change/data_stream/test_stream/agent/stream/stream.yml.hbs b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.2.5-non-breaking-change/data_stream/test_stream/agent/stream/stream.yml.hbs new file mode 100644 index 0000000000000..2870385f21f95 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.2.5-non-breaking-change/data_stream/test_stream/agent/stream/stream.yml.hbs @@ -0,0 +1 @@ +config.version: "2" diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.2.5-non-breaking-change/data_stream/test_stream/fields/fields.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.2.5-non-breaking-change/data_stream/test_stream/fields/fields.yml new file mode 100644 index 0000000000000..6e003ed0ad147 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.2.5-non-breaking-change/data_stream/test_stream/fields/fields.yml @@ -0,0 +1,16 @@ +- name: data_stream.type + type: constant_keyword + description: > + Data stream type. +- name: data_stream.dataset + type: constant_keyword + description: > + Data stream dataset. +- name: data_stream.namespace + type: constant_keyword + description: > + Data stream namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.2.5-non-breaking-change/data_stream/test_stream/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.2.5-non-breaking-change/data_stream/test_stream/manifest.yml new file mode 100644 index 0000000000000..95b72f0122aec --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.2.5-non-breaking-change/data_stream/test_stream/manifest.yml @@ -0,0 +1,15 @@ +title: Test stream +type: logs +streams: + - input: test_input + vars: + - name: test_var + type: text + title: Test Var + show_user: true + default: Test Value + - name: test_var_2 + type: text + title: Test Var 2 + show_user: true + default: Test Value 2 diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.2.5-non-breaking-change/docs/README.md b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.2.5-non-breaking-change/docs/README.md new file mode 100644 index 0000000000000..0b9b18421c9dc --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.2.5-non-breaking-change/docs/README.md @@ -0,0 +1,3 @@ +# Test package + +This is a test package for testing automated upgrades for package policies diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.2.5-non-breaking-change/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.2.5-non-breaking-change/manifest.yml new file mode 100644 index 0000000000000..2105ee451ffae --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.2.5-non-breaking-change/manifest.yml @@ -0,0 +1,23 @@ +format_version: 1.0.0 +name: package_policy_upgrade +title: Tests package policy upgrades +description: This is a test package for upgrading package policies +version: 0.2.5-non-breaking-change +categories: [] +release: beta +type: integration +license: basic +requirement: + elasticsearch: + versions: '>7.7.0' + kibana: + versions: '>7.7.0' +policy_templates: + - name: package_policy_upgrade + title: Package Policy Upgrade + description: Test Package for Upgrading Package Policies + inputs: + - type: test_input + title: Test Input + description: Test Input + enabled: true diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts b/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts index 3a7d6f5d6b19e..0be2d7d0a7468 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts @@ -162,6 +162,122 @@ export default function (providerContext: FtrProviderContext) { }); }); + describe('when upgrading to a version with no breaking changes', function () { + withTestPackageVersion('0.2.5-non-breaking-change'); + + beforeEach(async function () { + const { body: agentPolicyResponse } = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'Test policy', + namespace: 'default', + }) + .expect(200); + + agentPolicyId = agentPolicyResponse.item.id; + + const { body: packagePolicyResponse } = await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'package_policy_upgrade_1', + description: '', + namespace: 'default', + policy_id: agentPolicyId, + enabled: true, + output_id: '', + inputs: [ + { + policy_template: 'package_policy_upgrade', + type: 'test_input', + enabled: true, + streams: [ + { + id: 'test-package_policy_upgrade-xxxx', + enabled: true, + data_stream: { + type: 'test_stream', + dataset: 'package_policy_upgrade.test_stream', + }, + vars: { + test_var: { + value: 'My custom test value', + }, + }, + }, + ], + }, + ], + package: { + name: 'package_policy_upgrade', + title: 'This is a test package for upgrading package policies', + version: '0.2.0-add-non-required-test-var', + }, + }) + .expect(200); + + packagePolicyId = packagePolicyResponse.item.id; + }); + + afterEach(async function () { + await supertest + .post(`/api/fleet/package_policies/delete`) + .set('kbn-xsrf', 'xxxx') + .send({ packagePolicyIds: [packagePolicyId] }) + .expect(200); + + await supertest + .post('/api/fleet/agent_policies/delete') + .set('kbn-xsrf', 'xxxx') + .send({ agentPolicyId }) + .expect(200); + }); + + describe('dry run', function () { + it('returns a valid diff', async function () { + const { body }: { body: UpgradePackagePolicyDryRunResponse } = await supertest + .post(`/api/fleet/package_policies/upgrade`) + .set('kbn-xsrf', 'xxxx') + .send({ + packagePolicyIds: [packagePolicyId], + dryRun: true, + }) + .expect(200); + + expect(body.length).to.be(1); + expect(body[0].diff?.length).to.be(2); + expect(body[0].hasErrors).to.be(false); + + const [currentPackagePolicy, proposedPackagePolicy] = body[0].diff ?? []; + + expect(currentPackagePolicy?.package?.version).to.be('0.2.0-add-non-required-test-var'); + expect(proposedPackagePolicy?.package?.version).to.be('0.2.5-non-breaking-change'); + + const testInput = proposedPackagePolicy?.inputs.find(({ type }) => type === 'test_input'); + const testStream = testInput?.streams[0]; + + expect(testStream?.vars?.test_var.value).to.be('My custom test value'); + }); + }); + + describe('upgrade', function () { + it('successfully upgrades package policy', async function () { + const { body }: { body: UpgradePackagePolicyResponse } = await supertest + .post(`/api/fleet/package_policies/upgrade`) + .set('kbn-xsrf', 'xxxx') + .send({ + packagePolicyIds: [packagePolicyId], + dryRun: false, + }) + .expect(200); + + expect(body.length).to.be(1); + expect(body[0].success).to.be(true); + }); + }); + }); + describe('when upgrading to a version where a non-required variable has been added', function () { withTestPackageVersion('0.2.0-add-non-required-test-var'); diff --git a/x-pack/test/functional/apps/ingest_pipelines/feature_controls/ingest_pipelines_security.ts b/x-pack/test/functional/apps/ingest_pipelines/feature_controls/ingest_pipelines_security.ts index aed73d6c9858d..1f89ea8c635a6 100644 --- a/x-pack/test/functional/apps/ingest_pipelines/feature_controls/ingest_pipelines_security.ts +++ b/x-pack/test/functional/apps/ingest_pipelines/feature_controls/ingest_pipelines_security.ts @@ -60,7 +60,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('should render the "Ingest" section with ingest pipelines', async () => { await PageObjects.common.navigateToApp('management'); const sections = await managementMenu.getSections(); - // We gave the ingest node pipelines user access to advanced settings to allow them to use ingest node pipelines. + // We gave the ingest pipelines user access to advanced settings to allow them to use ingest pipelines. // See https://github.com/elastic/kibana/pull/102409/ expect(sections).to.have.length(2); expect(sections[0]).to.eql({ diff --git a/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts b/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts index 17b4fef06f5ce..026cea52e8102 100644 --- a/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts +++ b/x-pack/test/functional/apps/ingest_pipelines/ingest_pipelines.ts @@ -29,10 +29,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('Loads the app', async () => { - log.debug('Checking for section heading to say Ingest Node Pipelines.'); + log.debug('Checking for section heading to say Ingest Pipelines.'); const headingText = await pageObjects.ingestPipelines.sectionHeadingText(); - expect(headingText).to.be('Ingest Node Pipelines'); + expect(headingText).to.be('Ingest Pipelines'); }); it('Creates a pipeline', async () => { diff --git a/x-pack/test/functional/apps/visualize/reporting.ts b/x-pack/test/functional/apps/visualize/reporting.ts index f08d242f4024f..efffa0b6a692b 100644 --- a/x-pack/test/functional/apps/visualize/reporting.ts +++ b/x-pack/test/functional/apps/visualize/reporting.ts @@ -25,7 +25,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'visEditor', ]); - describe('Visualize Reporting Screenshots', () => { + // Failing: See https://github.com/elastic/kibana/issues/113496 + describe.skip('Visualize Reporting Screenshots', () => { before('initialize tests', async () => { log.debug('ReportingPage:initTests'); await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/reporting/ecommerce'); diff --git a/x-pack/test/plugin_api_integration/config.ts b/x-pack/test/plugin_api_integration/config.ts index cd13186a69cc6..0a9535df5a9f3 100644 --- a/x-pack/test/plugin_api_integration/config.ts +++ b/x-pack/test/plugin_api_integration/config.ts @@ -38,7 +38,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...integrationConfig.get('kbnTestServer'), serverArgs: [ ...integrationConfig.get('kbnTestServer.serverArgs'), - '--xpack.eventLog.enabled=true', '--xpack.eventLog.logEntries=true', '--xpack.eventLog.indexEntries=true', '--xpack.task_manager.monitored_aggregated_stats_refresh_rate=5000', diff --git a/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts b/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts index 5c27ffe62a48d..805feee159f27 100644 --- a/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts +++ b/x-pack/test/plugin_api_integration/plugins/event_log/server/init_routes.ts @@ -179,7 +179,7 @@ export const isEventLogServiceEnabledRoute = ( res: KibanaResponseFactory ): Promise<IKibanaResponse<any>> { logger.info(`test if event logger is enabled`); - return res.ok({ body: { isEnabled: eventLogService.isEnabled() } }); + return res.ok({ body: { isEnabled: true } }); } ); }; diff --git a/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts b/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts index 4c624cdbdda63..2c8564df02e0b 100644 --- a/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts +++ b/x-pack/test/plugin_api_integration/test_suites/event_log/service_api_integration.ts @@ -19,14 +19,6 @@ export default function ({ getService }: FtrProviderContext) { const retry = getService('retry'); describe('Event Log service API', () => { - it('should check if it is enabled', async () => { - const configValue = config - .get('kbnTestServer.serverArgs') - .find((val: string) => val === '--xpack.eventLog.enabled=true'); - const result = await isEventLogServiceEnabled(); - expect(configValue).to.be.eql(`--xpack.eventLog.enabled=${result.body.isEnabled}`); - }); - it('should check if logging entries is enabled', async () => { const configValue = config .get('kbnTestServer.serverArgs') @@ -216,14 +208,6 @@ export default function ({ getService }: FtrProviderContext) { .expect(200); } - async function isEventLogServiceEnabled() { - log.debug(`isEventLogServiceEnabled`); - return await supertest - .get(`/api/log_event_fixture/isEventLogServiceEnabled`) - .set('kbn-xsrf', 'foo') - .expect(200); - } - async function isEventLogServiceLoggingEntries() { log.debug(`isEventLogServiceLoggingEntries`); return await supertest diff --git a/x-pack/test/rule_registry/common/config.ts b/x-pack/test/rule_registry/common/config.ts index 487af84141d20..9cce58c30f6e9 100644 --- a/x-pack/test/rule_registry/common/config.ts +++ b/x-pack/test/rule_registry/common/config.ts @@ -83,7 +83,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) // TO DO: Remove feature flags once we're good to go '--xpack.securitySolution.enableExperimental=["ruleRegistryEnabled"]', '--xpack.ruleRegistry.write.enabled=true', - `--server.xsrf.whitelist=${JSON.stringify(getAllExternalServiceSimulatorPaths())}`, + `--server.xsrf.allowlist=${JSON.stringify(getAllExternalServiceSimulatorPaths())}`, ...(ssl ? [ `--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`, diff --git a/x-pack/test/timeline/common/config.ts b/x-pack/test/timeline/common/config.ts index ba1c8528527e4..fa8ddb2ad10a7 100644 --- a/x-pack/test/timeline/common/config.ts +++ b/x-pack/test/timeline/common/config.ts @@ -83,7 +83,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) // TO DO: Remove feature flags once we're good to go '--xpack.securitySolution.enableExperimental=["ruleRegistryEnabled"]', '--xpack.ruleRegistry.write.enabled=true', - `--server.xsrf.whitelist=${JSON.stringify(getAllExternalServiceSimulatorPaths())}`, + `--server.xsrf.allowlist=${JSON.stringify(getAllExternalServiceSimulatorPaths())}`, ...(ssl ? [ `--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`, diff --git a/yarn.lock b/yarn.lock index 9f6ccfffb8113..e7e369d4fbee2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2228,6 +2228,37 @@ enabled "2.0.x" kuler "^2.0.0" +"@dnd-kit/accessibility@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.0.0.tgz#b56e3750414fd907b7d6972b3116aa8f96d07fde" + integrity sha512-QwaQ1IJHQHMMuAGOOYHQSx7h7vMZPfO97aDts8t5N/MY7n2QTDSnW+kF7uRQ1tVBkr6vJ+BqHWG5dlgGvwVjow== + dependencies: + tslib "^2.0.0" + +"@dnd-kit/core@^3.1.1": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-3.1.1.tgz#c5ad6665931f5a51e74226220e58ac7514f3faf0" + integrity sha512-18YY5+1lTqJbGSg6JBSa/fjAOTUYAysFrQ5Ti8oppEPHFacQbC+owM51y2z2KN0LkDHBfGZKw2sFT7++ttwfpA== + dependencies: + "@dnd-kit/accessibility" "^3.0.0" + "@dnd-kit/utilities" "^2.0.0" + tslib "^2.0.0" + +"@dnd-kit/sortable@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@dnd-kit/sortable/-/sortable-4.0.0.tgz#81dd2b014a16527cf89602dc40060d9ee4dad352" + integrity sha512-teYVFy6mQG/u6F6CaGxAkzPfiNJvguFzWfJ/zonYQRxfANHX6QJ6GziMG9KON/Ae9Q2ODJP8vib+guWJrDXeGg== + dependencies: + "@dnd-kit/utilities" "^2.0.0" + tslib "^2.0.0" + +"@dnd-kit/utilities@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@dnd-kit/utilities/-/utilities-2.0.0.tgz#a8462dff65c6f606ecbe95273c7e263b14a1ab97" + integrity sha512-bjs49yMNzMM+BYRsBUhTqhTk6HEvhuY3leFt6Em6NaYGgygaMbtGbbXof/UXBv7rqyyi0OkmBBnrCCcxqS2t/g== + dependencies: + tslib "^2.0.0" + "@dsherret/to-absolute-glob@^2.0.2": version "2.0.2" resolved "https://registry.yarnpkg.com/@dsherret/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1f6475dc8bd974cea07a2daf3864b317b1dd332c"