diff --git a/.ci/Dockerfile b/.ci/Dockerfile
deleted file mode 100644
index 201e17b93c116..0000000000000
--- a/.ci/Dockerfile
+++ /dev/null
@@ -1,35 +0,0 @@
-ARG NODE_VERSION=10.21.0
-
-FROM node:${NODE_VERSION} AS base
-
-RUN apt-get update && \
- apt-get -y install xvfb gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 \
- libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 \
- libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \
- libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 \
- libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget openjdk-8-jre && \
- rm -rf /var/lib/apt/lists/*
-
-RUN curl -sSL https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \
- && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
- && apt-get update \
- && apt-get install -y rsync jq bsdtar google-chrome-stable \
- --no-install-recommends \
- && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
-
-RUN LATEST_VAULT_RELEASE=$(curl -s https://api.github.com/repos/hashicorp/vault/tags | jq --raw-output .[0].name[1:]) \
- && curl -L https://releases.hashicorp.com/vault/${LATEST_VAULT_RELEASE}/vault_${LATEST_VAULT_RELEASE}_linux_amd64.zip -o vault.zip \
- && unzip vault.zip \
- && rm vault.zip \
- && chmod +x vault \
- && mv vault /usr/local/bin/vault
-
-RUN groupadd -r kibana && useradd -r -g kibana kibana && mkdir /home/kibana && chown kibana:kibana /home/kibana
-
-COPY ./bash_standard_lib.sh /usr/local/bin/bash_standard_lib.sh
-RUN chmod +x /usr/local/bin/bash_standard_lib.sh
-
-COPY ./runbld /usr/local/bin/runbld
-RUN chmod +x /usr/local/bin/runbld
-
-USER kibana
diff --git a/.ci/Jenkinsfile_visual_baseline b/.ci/Jenkinsfile_visual_baseline
index 2a16c499fa168..7c7cc8d98c306 100644
--- a/.ci/Jenkinsfile_visual_baseline
+++ b/.ci/Jenkinsfile_visual_baseline
@@ -21,5 +21,6 @@ kibanaPipeline(timeoutMinutes: 120) {
}
kibanaPipeline.sendMail()
+ slackNotifications.onFailure()
}
}
diff --git a/.ci/packer_cache_for_branch.sh b/.ci/packer_cache_for_branch.sh
index a9fbe781915b6..5b4a94be50fa2 100755
--- a/.ci/packer_cache_for_branch.sh
+++ b/.ci/packer_cache_for_branch.sh
@@ -46,7 +46,7 @@ echo "Creating bootstrap_cache archive"
# archive cacheable directories
mkdir -p "$HOME/.kibana/bootstrap_cache"
tar -cf "$HOME/.kibana/bootstrap_cache/$branch.tar" \
- x-pack/plugins/reporting/.chromium \
+ .chromium \
.es \
.chromedriver \
.geckodriver;
diff --git a/.ci/runbld_no_junit.yml b/.ci/runbld_no_junit.yml
index 1bcb7e22a2648..67b5002c1c437 100644
--- a/.ci/runbld_no_junit.yml
+++ b/.ci/runbld_no_junit.yml
@@ -3,4 +3,4 @@
profiles:
- ".*": # Match any job
tests:
- junit-filename-pattern: false
+ junit-filename-pattern: "8d8bd494-d909-4e67-a052-7e8b5aaeb5e4" # A bogus path that should never exist
diff --git a/.eslintignore b/.eslintignore
index 9de2cc2872960..4b5e781c26971 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,6 +1,7 @@
**/*.js.snap
**/graphql/types.ts
/.es
+/.chromium
/build
/built_assets
/config/apm.dev.js
diff --git a/.eslintrc.js b/.eslintrc.js
index 8d979dc0f8645..4425ad3a12659 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -906,6 +906,18 @@ module.exports = {
},
},
+ /**
+ * Enterprise Search overrides
+ */
+ {
+ files: ['x-pack/plugins/enterprise_search/**/*.{ts,tsx}'],
+ excludedFiles: ['x-pack/plugins/enterprise_search/**/*.{test,mock}.{ts,tsx}'],
+ rules: {
+ 'react-hooks/exhaustive-deps': 'off',
+ '@typescript-eslint/no-explicit-any': 'error',
+ },
+ },
+
/**
* disable jsx-a11y for kbn-ui-framework
*/
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 4aab9943022d4..f053c6da9c29b 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -201,6 +201,11 @@ x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @elastic/kib
# Design
**/*.scss @elastic/kibana-design
+# Enterprise Search
+/x-pack/plugins/enterprise_search/ @elastic/app-search-frontend @elastic/workplace-search-frontend
+/x-pack/test/functional_enterprise_search/ @elastic/app-search-frontend @elastic/workplace-search-frontend
+/x-pack/plugins/enterprise_search/**/*.scss @elastic/ent-search-design
+
# Elasticsearch UI
/src/plugins/dev_tools/ @elastic/es-ui
/src/plugins/console/ @elastic/es-ui
diff --git a/.gitignore b/.gitignore
index 25a8c369bb704..716cea937f9c0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
.signing-config.json
.ackrc
/.es
+/.chromium
.DS_Store
.node_binaries
.native_modules
@@ -47,8 +48,6 @@ npm-debug.log*
.tern-project
.nyc_output
.ci/pipeline-library/build/
-.ci/runbld
-.ci/bash_standard_lib.sh
.gradle
# apm plugin
diff --git a/Jenkinsfile b/Jenkinsfile
index 491a1e386deb1..f6f77ccae8427 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -8,7 +8,50 @@ kibanaPipeline(timeoutMinutes: 155, checkPrChanges: true, setCommitStatus: true)
ciStats.trackBuild {
catchError {
retryable.enable()
- kibanaPipeline.allCiTasks()
+ parallel([
+ 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'),
+ 'x-pack-intake-agent': workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh'),
+ 'kibana-oss-agent': workers.functional('kibana-oss-tests', { kibanaPipeline.buildOss() }, [
+ 'oss-firefoxSmoke': kibanaPipeline.functionalTestProcess('kibana-firefoxSmoke', './test/scripts/jenkins_firefox_smoke.sh'),
+ 'oss-ciGroup1': kibanaPipeline.ossCiGroupProcess(1),
+ 'oss-ciGroup2': kibanaPipeline.ossCiGroupProcess(2),
+ 'oss-ciGroup3': kibanaPipeline.ossCiGroupProcess(3),
+ 'oss-ciGroup4': kibanaPipeline.ossCiGroupProcess(4),
+ 'oss-ciGroup5': kibanaPipeline.ossCiGroupProcess(5),
+ 'oss-ciGroup6': kibanaPipeline.ossCiGroupProcess(6),
+ 'oss-ciGroup7': kibanaPipeline.ossCiGroupProcess(7),
+ 'oss-ciGroup8': kibanaPipeline.ossCiGroupProcess(8),
+ 'oss-ciGroup9': kibanaPipeline.ossCiGroupProcess(9),
+ 'oss-ciGroup10': kibanaPipeline.ossCiGroupProcess(10),
+ 'oss-ciGroup11': kibanaPipeline.ossCiGroupProcess(11),
+ 'oss-ciGroup12': kibanaPipeline.ossCiGroupProcess(12),
+ 'oss-accessibility': kibanaPipeline.functionalTestProcess('kibana-accessibility', './test/scripts/jenkins_accessibility.sh'),
+ // 'oss-visualRegression': kibanaPipeline.functionalTestProcess('visualRegression', './test/scripts/jenkins_visual_regression.sh'),
+ ]),
+ 'kibana-xpack-agent': workers.functional('kibana-xpack-tests', { kibanaPipeline.buildXpack() }, [
+ 'xpack-firefoxSmoke': kibanaPipeline.functionalTestProcess('xpack-firefoxSmoke', './test/scripts/jenkins_xpack_firefox_smoke.sh'),
+ 'xpack-ciGroup1': kibanaPipeline.xpackCiGroupProcess(1),
+ 'xpack-ciGroup2': kibanaPipeline.xpackCiGroupProcess(2),
+ 'xpack-ciGroup3': kibanaPipeline.xpackCiGroupProcess(3),
+ 'xpack-ciGroup4': kibanaPipeline.xpackCiGroupProcess(4),
+ 'xpack-ciGroup5': kibanaPipeline.xpackCiGroupProcess(5),
+ 'xpack-ciGroup6': kibanaPipeline.xpackCiGroupProcess(6),
+ 'xpack-ciGroup7': kibanaPipeline.xpackCiGroupProcess(7),
+ 'xpack-ciGroup8': kibanaPipeline.xpackCiGroupProcess(8),
+ 'xpack-ciGroup9': kibanaPipeline.xpackCiGroupProcess(9),
+ 'xpack-ciGroup10': kibanaPipeline.xpackCiGroupProcess(10),
+ 'xpack-accessibility': kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh'),
+ 'xpack-savedObjectsFieldMetrics': kibanaPipeline.functionalTestProcess('xpack-savedObjectsFieldMetrics', './test/scripts/jenkins_xpack_saved_objects_field_metrics.sh'),
+ // 'xpack-pageLoadMetrics': kibanaPipeline.functionalTestProcess('xpack-pageLoadMetrics', './test/scripts/jenkins_xpack_page_load_metrics.sh'),
+ 'xpack-securitySolutionCypress': { processNumber ->
+ whenChanged(['x-pack/plugins/security_solution/', 'x-pack/test/security_solution_cypress/']) {
+ kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypress', './test/scripts/jenkins_security_solution_cypress.sh')(processNumber)
+ }
+ },
+
+ // 'xpack-visualRegression': kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh'),
+ ]),
+ ])
}
}
}
diff --git a/docs/apm/api.asciidoc b/docs/apm/api.asciidoc
index f6bc83d4086c2..97fdcd3e13de9 100644
--- a/docs/apm/api.asciidoc
+++ b/docs/apm/api.asciidoc
@@ -398,7 +398,7 @@ include::api.asciidoc[tag=using-the-APIs]
[%collapsible%open]
======
`version` :::
- (required, string) Name of service.
+ (required, string) Version of service.
`environment` :::
(optional, string) Environment of service.
diff --git a/docs/images/canvas-add-image.gif b/docs/canvas/images/canvas-add-image.gif
similarity index 100%
rename from docs/images/canvas-add-image.gif
rename to docs/canvas/images/canvas-add-image.gif
diff --git a/docs/images/canvas-add-pages.gif b/docs/canvas/images/canvas-add-pages.gif
similarity index 100%
rename from docs/images/canvas-add-pages.gif
rename to docs/canvas/images/canvas-add-pages.gif
diff --git a/docs/images/canvas-autoplay-interval.png b/docs/canvas/images/canvas-autoplay-interval.png
similarity index 100%
rename from docs/images/canvas-autoplay-interval.png
rename to docs/canvas/images/canvas-autoplay-interval.png
diff --git a/docs/images/canvas-background-color-picker.png b/docs/canvas/images/canvas-background-color-picker.png
similarity index 100%
rename from docs/images/canvas-background-color-picker.png
rename to docs/canvas/images/canvas-background-color-picker.png
diff --git a/docs/images/canvas-change-your-expression-chart-no-legend.png b/docs/canvas/images/canvas-change-your-expression-chart-no-legend.png
similarity index 100%
rename from docs/images/canvas-change-your-expression-chart-no-legend.png
rename to docs/canvas/images/canvas-change-your-expression-chart-no-legend.png
diff --git a/docs/images/canvas-change-your-expression-chart.png b/docs/canvas/images/canvas-change-your-expression-chart.png
similarity index 100%
rename from docs/images/canvas-change-your-expression-chart.png
rename to docs/canvas/images/canvas-change-your-expression-chart.png
diff --git a/docs/images/canvas-chart-element.png b/docs/canvas/images/canvas-chart-element.png
similarity index 100%
rename from docs/images/canvas-chart-element.png
rename to docs/canvas/images/canvas-chart-element.png
diff --git a/docs/images/canvas-create-URL.gif b/docs/canvas/images/canvas-create-URL.gif
similarity index 100%
rename from docs/images/canvas-create-URL.gif
rename to docs/canvas/images/canvas-create-URL.gif
diff --git a/docs/images/canvas-element-select.gif b/docs/canvas/images/canvas-element-select.gif
similarity index 100%
rename from docs/images/canvas-element-select.gif
rename to docs/canvas/images/canvas-element-select.gif
diff --git a/docs/images/canvas-export-workpad.png b/docs/canvas/images/canvas-export-workpad.png
similarity index 100%
rename from docs/images/canvas-export-workpad.png
rename to docs/canvas/images/canvas-export-workpad.png
diff --git a/docs/images/canvas-fullscreen.png b/docs/canvas/images/canvas-fullscreen.png
similarity index 100%
rename from docs/images/canvas-fullscreen.png
rename to docs/canvas/images/canvas-fullscreen.png
diff --git a/docs/images/canvas-functions-can-take-arguments-donut-chart.png b/docs/canvas/images/canvas-functions-can-take-arguments-donut-chart.png
similarity index 100%
rename from docs/images/canvas-functions-can-take-arguments-donut-chart.png
rename to docs/canvas/images/canvas-functions-can-take-arguments-donut-chart.png
diff --git a/docs/images/canvas-functions-can-take-arguments-pie-chart.png b/docs/canvas/images/canvas-functions-can-take-arguments-pie-chart.png
similarity index 100%
rename from docs/images/canvas-functions-can-take-arguments-pie-chart.png
rename to docs/canvas/images/canvas-functions-can-take-arguments-pie-chart.png
diff --git a/docs/images/canvas-generate-pdf.gif b/docs/canvas/images/canvas-generate-pdf.gif
similarity index 100%
rename from docs/images/canvas-generate-pdf.gif
rename to docs/canvas/images/canvas-generate-pdf.gif
diff --git a/docs/images/canvas-gs-example.png b/docs/canvas/images/canvas-gs-example.png
similarity index 100%
rename from docs/images/canvas-gs-example.png
rename to docs/canvas/images/canvas-gs-example.png
diff --git a/docs/images/canvas-image-element.png b/docs/canvas/images/canvas-image-element.png
similarity index 100%
rename from docs/images/canvas-image-element.png
rename to docs/canvas/images/canvas-image-element.png
diff --git a/docs/images/canvas-map-embed.gif b/docs/canvas/images/canvas-map-embed.gif
similarity index 100%
rename from docs/images/canvas-map-embed.gif
rename to docs/canvas/images/canvas-map-embed.gif
diff --git a/docs/images/canvas-metric-element.png b/docs/canvas/images/canvas-metric-element.png
similarity index 100%
rename from docs/images/canvas-metric-element.png
rename to docs/canvas/images/canvas-metric-element.png
diff --git a/docs/images/canvas-refresh-interval.png b/docs/canvas/images/canvas-refresh-interval.png
similarity index 100%
rename from docs/images/canvas-refresh-interval.png
rename to docs/canvas/images/canvas-refresh-interval.png
diff --git a/docs/images/canvas-timefilter-element.png b/docs/canvas/images/canvas-timefilter-element.png
similarity index 100%
rename from docs/images/canvas-timefilter-element.png
rename to docs/canvas/images/canvas-timefilter-element.png
diff --git a/docs/images/canvas-zoom-controls.png b/docs/canvas/images/canvas-zoom-controls.png
similarity index 100%
rename from docs/images/canvas-zoom-controls.png
rename to docs/canvas/images/canvas-zoom-controls.png
diff --git a/docs/images/canvas_element_options.png b/docs/canvas/images/canvas_element_options.png
similarity index 100%
rename from docs/images/canvas_element_options.png
rename to docs/canvas/images/canvas_element_options.png
diff --git a/docs/images/canvas_save_element.png b/docs/canvas/images/canvas_save_element.png
similarity index 100%
rename from docs/images/canvas_save_element.png
rename to docs/canvas/images/canvas_save_element.png
diff --git a/docs/images/settings.png b/docs/dev-tools/console/images/settings.png
similarity index 100%
rename from docs/images/settings.png
rename to docs/dev-tools/console/images/settings.png
diff --git a/docs/images/jenkins/job_view.png b/docs/developer/images/job_view.png
similarity index 100%
rename from docs/images/jenkins/job_view.png
rename to docs/developer/images/job_view.png
diff --git a/docs/images/jenkins/pipeline_steps_view.png b/docs/developer/images/pipeline_steps_view.png
similarity index 100%
rename from docs/images/jenkins/pipeline_steps_view.png
rename to docs/developer/images/pipeline_steps_view.png
diff --git a/docs/developer/testing/interpreting-ci-failures.asciidoc b/docs/developer/testing/interpreting-ci-failures.asciidoc
index bc237928cf5aa..c47a59217d89b 100644
--- a/docs/developer/testing/interpreting-ci-failures.asciidoc
+++ b/docs/developer/testing/interpreting-ci-failures.asciidoc
@@ -17,7 +17,7 @@ Clicking the link next to the check in the conversation tab of a pull request wi
To view the results of a job execution in Jenkins, either click the link in the comment left by `@elasticmachine` or search for the `kibana-ci` check in the list at the bottom of the PR. This link will take you to the top-level page for the specific job execution that failed.
-image::images/jenkins/job_view.png[]
+image::images/job_view.png[]
1. *Git Changes:* the list of commits that were in this build which weren't in the previous build. For Pull Requests this list is calculated by comparing against the most recent Pull Request which was tested, it is not limited to build for this specific Pull Request, so it's not very useful.
2. *Test Results:* A link to the test results screen, and shortcuts to the failed tests. Functional tests capture and store the log output from each specific test, and make it visible at these links. For other test runners only the error message is visible and log output must be tracked down in the *Pipeline Steps*.
@@ -29,6 +29,6 @@ image::images/jenkins/job_view.png[]
To view the logs for a failed specific ciGroup, jest, mocha, type checkers, linters, etc., click on the *Pipeline Steps* link in from the Job page.
-image::images/jenkins/pipeline_steps_view.png[]
+image::images/pipeline_steps_view.png[]
Scroll down the page until you find a failed step *(1)*, and then look up a few lines for the `Branch:` step to see which specific job this is. If this is the job you're looking for click the little terminal icon next to the failed step *(1)* to view the logs for that specific step in the Pipeline.
\ No newline at end of file
diff --git a/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.md b/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.md
index 0e2b9bd60ab67..b88a179c5c4b3 100644
--- a/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.md
+++ b/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.md
@@ -19,5 +19,6 @@ export interface DiscoveredPlugin
| [configPath](./kibana-plugin-core-server.discoveredplugin.configpath.md) | ConfigPath
| Root configuration path used by the plugin, defaults to "id" in snake\_case format. |
| [id](./kibana-plugin-core-server.discoveredplugin.id.md) | PluginName
| Identifier of the plugin. |
| [optionalPlugins](./kibana-plugin-core-server.discoveredplugin.optionalplugins.md) | readonly PluginName[]
| An optional list of the other plugins that if installed and enabled \*\*may be\*\* leveraged by this plugin for some additional functionality but otherwise are not required for this plugin to work properly. |
+| [requiredBundles](./kibana-plugin-core-server.discoveredplugin.requiredbundles.md) | readonly PluginName[]
| List of plugin ids that this plugin's UI code imports modules from that are not in requiredPlugins
. |
| [requiredPlugins](./kibana-plugin-core-server.discoveredplugin.requiredplugins.md) | readonly PluginName[]
| An optional list of the other plugins that \*\*must be\*\* installed and enabled for this plugin to function properly. |
diff --git a/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.requiredbundles.md b/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.requiredbundles.md
new file mode 100644
index 0000000000000..6d54adb5236ea
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.discoveredplugin.requiredbundles.md
@@ -0,0 +1,18 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DiscoveredPlugin](./kibana-plugin-core-server.discoveredplugin.md) > [requiredBundles](./kibana-plugin-core-server.discoveredplugin.requiredbundles.md)
+
+## DiscoveredPlugin.requiredBundles property
+
+List of plugin ids that this plugin's UI code imports modules from that are not in `requiredPlugins`.
+
+Signature:
+
+```typescript
+readonly requiredBundles: readonly PluginName[];
+```
+
+## Remarks
+
+The plugins listed here will be loaded in the browser, even if the plugin is disabled. Required by `@kbn/optimizer` to support cross-plugin imports. "core" and plugins already listed in `requiredPlugins` do not need to be duplicated here.
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md
index 7ebd0531619fd..8d4c0c915437e 100644
--- a/docs/development/core/server/kibana-plugin-core-server.md
+++ b/docs/development/core/server/kibana-plugin-core-server.md
@@ -154,7 +154,7 @@ The plugin integrates with the core system via lifecycle events: `setup`
| [SavedObjectsBulkUpdateResponse](./kibana-plugin-core-server.savedobjectsbulkupdateresponse.md) | |
| [SavedObjectsClientProviderOptions](./kibana-plugin-core-server.savedobjectsclientprovideroptions.md) | Options to control the creation of the Saved Objects Client. |
| [SavedObjectsClientWrapperOptions](./kibana-plugin-core-server.savedobjectsclientwrapperoptions.md) | Options passed to each SavedObjectsClientWrapperFactory to aid in creating the wrapper instance. |
-| [SavedObjectsComplexFieldMapping](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.md) | See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation.Note: this type intentially doesn't include a type definition for defining the dynamic
mapping parameter. Saved Object fields should always inherit the dynamic: 'strict'
paramater. If you are unsure of the shape of your data use type: 'object', enabled: false
instead. |
+| [SavedObjectsComplexFieldMapping](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.md) | See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation. |
| [SavedObjectsCoreFieldMapping](./kibana-plugin-core-server.savedobjectscorefieldmapping.md) | See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation. |
| [SavedObjectsCreateOptions](./kibana-plugin-core-server.savedobjectscreateoptions.md) | |
| [SavedObjectsDeleteByNamespaceOptions](./kibana-plugin-core-server.savedobjectsdeletebynamespaceoptions.md) | |
diff --git a/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md b/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md
index 5edee51d6c523..6db2f89590149 100644
--- a/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md
+++ b/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.md
@@ -25,6 +25,7 @@ Should never be used in code outside of Core but is exported for documentation p
| [id](./kibana-plugin-core-server.pluginmanifest.id.md) | PluginName
| Identifier of the plugin. Must be a string in camelCase. Part of a plugin public contract. Other plugins leverage it to access plugin API, navigate to the plugin, etc. |
| [kibanaVersion](./kibana-plugin-core-server.pluginmanifest.kibanaversion.md) | string
| The version of Kibana the plugin is compatible with, defaults to "version". |
| [optionalPlugins](./kibana-plugin-core-server.pluginmanifest.optionalplugins.md) | readonly PluginName[]
| An optional list of the other plugins that if installed and enabled \*\*may be\*\* leveraged by this plugin for some additional functionality but otherwise are not required for this plugin to work properly. |
+| [requiredBundles](./kibana-plugin-core-server.pluginmanifest.requiredbundles.md) | readonly string[]
| List of plugin ids that this plugin's UI code imports modules from that are not in requiredPlugins
. |
| [requiredPlugins](./kibana-plugin-core-server.pluginmanifest.requiredplugins.md) | readonly PluginName[]
| An optional list of the other plugins that \*\*must be\*\* installed and enabled for this plugin to function properly. |
| [server](./kibana-plugin-core-server.pluginmanifest.server.md) | boolean
| Specifies whether plugin includes some server-side specific functionality. |
| [ui](./kibana-plugin-core-server.pluginmanifest.ui.md) | boolean
| Specifies whether plugin includes some client/browser specific functionality that should be included into client bundle via public/ui_plugin.js
file. |
diff --git a/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.requiredbundles.md b/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.requiredbundles.md
new file mode 100644
index 0000000000000..98505d07101fe
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.pluginmanifest.requiredbundles.md
@@ -0,0 +1,18 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [PluginManifest](./kibana-plugin-core-server.pluginmanifest.md) > [requiredBundles](./kibana-plugin-core-server.pluginmanifest.requiredbundles.md)
+
+## PluginManifest.requiredBundles property
+
+List of plugin ids that this plugin's UI code imports modules from that are not in `requiredPlugins`.
+
+Signature:
+
+```typescript
+readonly requiredBundles: readonly string[];
+```
+
+## Remarks
+
+The plugins listed here will be loaded in the browser, even if the plugin is disabled. Required by `@kbn/optimizer` to support cross-plugin imports. "core" and plugins already listed in `requiredPlugins` do not need to be duplicated here.
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.dynamic.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.dynamic.md
new file mode 100644
index 0000000000000..b01da3c62fda6
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.dynamic.md
@@ -0,0 +1,15 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsComplexFieldMapping](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.md) > [dynamic](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.dynamic.md)
+
+## SavedObjectsComplexFieldMapping.dynamic property
+
+The dynamic property of the mapping, either `false` or `'strict'`. If unspecified `dynamic: 'strict'` will be inherited from the top-level index mappings.
+
+Note: To limit the number of mapping fields Saved Object types should \*never\* use `dynamic: true`.
+
+Signature:
+
+```typescript
+dynamic?: false | 'strict';
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.enabled.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.enabled.md
new file mode 100644
index 0000000000000..08513aa2a849b
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.enabled.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsComplexFieldMapping](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.md) > [enabled](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.enabled.md)
+
+## SavedObjectsComplexFieldMapping.enabled property
+
+Signature:
+
+```typescript
+enabled?: boolean;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.md
index cb81686b424ec..fc262cad54f18 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscomplexfieldmapping.md
@@ -6,8 +6,6 @@
See [SavedObjectsFieldMapping](./kibana-plugin-core-server.savedobjectsfieldmapping.md) for documentation.
-Note: this type intentially doesn't include a type definition for defining the `dynamic` mapping parameter. Saved Object fields should always inherit the `dynamic: 'strict'` paramater. If you are unsure of the shape of your data use `type: 'object', enabled: false` instead.
-
Signature:
```typescript
@@ -19,6 +17,8 @@ export interface SavedObjectsComplexFieldMapping
| Property | Type | Description |
| --- | --- | --- |
| [doc\_values](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.doc_values.md) | boolean
| |
+| [dynamic](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.dynamic.md) | false | 'strict'
| The dynamic property of the mapping, either false
or 'strict'
. If unspecified dynamic: 'strict'
will be inherited from the top-level index mappings.Note: To limit the number of mapping fields Saved Object types should \*never\* use dynamic: true
. |
+| [enabled](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.enabled.md) | boolean
| |
| [properties](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.properties.md) | SavedObjectsMappingProperties
| |
| [type](./kibana-plugin-core-server.savedobjectscomplexfieldmapping.type.md) | string
| |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.enabled.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.enabled.md
deleted file mode 100644
index c0b556e99ebc3..0000000000000
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.enabled.md
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsCoreFieldMapping](./kibana-plugin-core-server.savedobjectscorefieldmapping.md) > [enabled](./kibana-plugin-core-server.savedobjectscorefieldmapping.enabled.md)
-
-## SavedObjectsCoreFieldMapping.enabled property
-
-Signature:
-
-```typescript
-enabled?: boolean;
-```
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md
index b9e726eac799d..e9b9c2bcf51b5 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md
@@ -17,7 +17,6 @@ export interface SavedObjectsCoreFieldMapping
| Property | Type | Description |
| --- | --- | --- |
| [doc\_values](./kibana-plugin-core-server.savedobjectscorefieldmapping.doc_values.md) | boolean
| |
-| [enabled](./kibana-plugin-core-server.savedobjectscorefieldmapping.enabled.md) | boolean
| |
| [fields](./kibana-plugin-core-server.savedobjectscorefieldmapping.fields.md) | {
[subfield: string]: {
type: string;
ignore_above?: number;
};
}
| |
| [index](./kibana-plugin-core-server.savedobjectscorefieldmapping.index.md) | boolean
| |
| [null\_value](./kibana-plugin-core-server.savedobjectscorefieldmapping.null_value.md) | number | boolean | string
| |
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemappingdefinition.dynamic.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemappingdefinition.dynamic.md
index 74efa75768f9c..70775760ac77d 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemappingdefinition.dynamic.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemappingdefinition.dynamic.md
@@ -4,7 +4,7 @@
## SavedObjectsTypeMappingDefinition.dynamic property
-The dynamic property of the mapping. either `false` or 'strict'. Defaults to `false`
+The dynamic property of the mapping, either `false` or `'strict'`. If unspecified `dynamic: 'strict'` will be inherited from the top-level index mappings.
Signature:
diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemappingdefinition.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemappingdefinition.md
index 77ded4389c0a0..3d3b73880fa7f 100644
--- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemappingdefinition.md
+++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemappingdefinition.md
@@ -41,6 +41,6 @@ const typeDefinition: SavedObjectsTypeMappingDefinition = {
| Property | Type | Description |
| --- | --- | --- |
-| [dynamic](./kibana-plugin-core-server.savedobjectstypemappingdefinition.dynamic.md) | false | 'strict'
| The dynamic property of the mapping. either false
or 'strict'. Defaults to false
|
+| [dynamic](./kibana-plugin-core-server.savedobjectstypemappingdefinition.dynamic.md) | false | 'strict'
| The dynamic property of the mapping, either false
or 'strict'
. If unspecified dynamic: 'strict'
will be inherited from the top-level index mappings. |
| [properties](./kibana-plugin-core-server.savedobjectstypemappingdefinition.properties.md) | SavedObjectsMappingProperties
| The underlying properties of the type mapping |
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md
index a48f4920b3d26..e515c3513df6c 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md
@@ -8,32 +8,33 @@
```typescript
UI_SETTINGS: {
- META_FIELDS: string;
- DOC_HIGHLIGHT: string;
- QUERY_STRING_OPTIONS: string;
- QUERY_ALLOW_LEADING_WILDCARDS: string;
- SEARCH_QUERY_LANGUAGE: string;
- SORT_OPTIONS: string;
- COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: string;
- COURIER_SET_REQUEST_PREFERENCE: string;
- COURIER_CUSTOM_REQUEST_PREFERENCE: string;
- COURIER_MAX_CONCURRENT_SHARD_REQUESTS: string;
- COURIER_BATCH_SEARCHES: string;
- SEARCH_INCLUDE_FROZEN: string;
- HISTOGRAM_BAR_TARGET: string;
- HISTOGRAM_MAX_BARS: string;
- HISTORY_LIMIT: string;
- SHORT_DOTS_ENABLE: string;
- FORMAT_DEFAULT_TYPE_MAP: string;
- FORMAT_NUMBER_DEFAULT_PATTERN: string;
- FORMAT_PERCENT_DEFAULT_PATTERN: string;
- FORMAT_BYTES_DEFAULT_PATTERN: string;
- FORMAT_CURRENCY_DEFAULT_PATTERN: string;
- FORMAT_NUMBER_DEFAULT_LOCALE: string;
- TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: string;
- TIMEPICKER_QUICK_RANGES: string;
- INDEXPATTERN_PLACEHOLDER: string;
- FILTERS_PINNED_BY_DEFAULT: string;
- FILTERS_EDITOR_SUGGEST_VALUES: string;
+ readonly META_FIELDS: "metaFields";
+ readonly DOC_HIGHLIGHT: "doc_table:highlight";
+ readonly QUERY_STRING_OPTIONS: "query:queryString:options";
+ readonly QUERY_ALLOW_LEADING_WILDCARDS: "query:allowLeadingWildcards";
+ readonly SEARCH_QUERY_LANGUAGE: "search:queryLanguage";
+ readonly SORT_OPTIONS: "sort:options";
+ readonly COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: "courier:ignoreFilterIfFieldNotInIndex";
+ readonly COURIER_SET_REQUEST_PREFERENCE: "courier:setRequestPreference";
+ readonly COURIER_CUSTOM_REQUEST_PREFERENCE: "courier:customRequestPreference";
+ readonly COURIER_MAX_CONCURRENT_SHARD_REQUESTS: "courier:maxConcurrentShardRequests";
+ readonly COURIER_BATCH_SEARCHES: "courier:batchSearches";
+ readonly SEARCH_INCLUDE_FROZEN: "search:includeFrozen";
+ readonly HISTOGRAM_BAR_TARGET: "histogram:barTarget";
+ readonly HISTOGRAM_MAX_BARS: "histogram:maxBars";
+ readonly HISTORY_LIMIT: "history:limit";
+ readonly SHORT_DOTS_ENABLE: "shortDots:enable";
+ readonly FORMAT_DEFAULT_TYPE_MAP: "format:defaultTypeMap";
+ readonly FORMAT_NUMBER_DEFAULT_PATTERN: "format:number:defaultPattern";
+ readonly FORMAT_PERCENT_DEFAULT_PATTERN: "format:percent:defaultPattern";
+ readonly FORMAT_BYTES_DEFAULT_PATTERN: "format:bytes:defaultPattern";
+ readonly FORMAT_CURRENCY_DEFAULT_PATTERN: "format:currency:defaultPattern";
+ readonly FORMAT_NUMBER_DEFAULT_LOCALE: "format:number:defaultLocale";
+ readonly TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: "timepicker:refreshIntervalDefaults";
+ readonly TIMEPICKER_QUICK_RANGES: "timepicker:quickRanges";
+ readonly TIMEPICKER_TIME_DEFAULTS: "timepicker:timeDefaults";
+ readonly INDEXPATTERN_PLACEHOLDER: "indexPattern:placeholder";
+ readonly FILTERS_PINNED_BY_DEFAULT: "filters:pinnedByDefault";
+ readonly FILTERS_EDITOR_SUGGEST_VALUES: "filterEditor:suggestValues";
}
```
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md
index 855cfd11d00ea..e419b64cd43aa 100644
--- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md
@@ -8,32 +8,33 @@
```typescript
UI_SETTINGS: {
- META_FIELDS: string;
- DOC_HIGHLIGHT: string;
- QUERY_STRING_OPTIONS: string;
- QUERY_ALLOW_LEADING_WILDCARDS: string;
- SEARCH_QUERY_LANGUAGE: string;
- SORT_OPTIONS: string;
- COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: string;
- COURIER_SET_REQUEST_PREFERENCE: string;
- COURIER_CUSTOM_REQUEST_PREFERENCE: string;
- COURIER_MAX_CONCURRENT_SHARD_REQUESTS: string;
- COURIER_BATCH_SEARCHES: string;
- SEARCH_INCLUDE_FROZEN: string;
- HISTOGRAM_BAR_TARGET: string;
- HISTOGRAM_MAX_BARS: string;
- HISTORY_LIMIT: string;
- SHORT_DOTS_ENABLE: string;
- FORMAT_DEFAULT_TYPE_MAP: string;
- FORMAT_NUMBER_DEFAULT_PATTERN: string;
- FORMAT_PERCENT_DEFAULT_PATTERN: string;
- FORMAT_BYTES_DEFAULT_PATTERN: string;
- FORMAT_CURRENCY_DEFAULT_PATTERN: string;
- FORMAT_NUMBER_DEFAULT_LOCALE: string;
- TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: string;
- TIMEPICKER_QUICK_RANGES: string;
- INDEXPATTERN_PLACEHOLDER: string;
- FILTERS_PINNED_BY_DEFAULT: string;
- FILTERS_EDITOR_SUGGEST_VALUES: string;
+ readonly META_FIELDS: "metaFields";
+ readonly DOC_HIGHLIGHT: "doc_table:highlight";
+ readonly QUERY_STRING_OPTIONS: "query:queryString:options";
+ readonly QUERY_ALLOW_LEADING_WILDCARDS: "query:allowLeadingWildcards";
+ readonly SEARCH_QUERY_LANGUAGE: "search:queryLanguage";
+ readonly SORT_OPTIONS: "sort:options";
+ readonly COURIER_IGNORE_FILTER_IF_FIELD_NOT_IN_INDEX: "courier:ignoreFilterIfFieldNotInIndex";
+ readonly COURIER_SET_REQUEST_PREFERENCE: "courier:setRequestPreference";
+ readonly COURIER_CUSTOM_REQUEST_PREFERENCE: "courier:customRequestPreference";
+ readonly COURIER_MAX_CONCURRENT_SHARD_REQUESTS: "courier:maxConcurrentShardRequests";
+ readonly COURIER_BATCH_SEARCHES: "courier:batchSearches";
+ readonly SEARCH_INCLUDE_FROZEN: "search:includeFrozen";
+ readonly HISTOGRAM_BAR_TARGET: "histogram:barTarget";
+ readonly HISTOGRAM_MAX_BARS: "histogram:maxBars";
+ readonly HISTORY_LIMIT: "history:limit";
+ readonly SHORT_DOTS_ENABLE: "shortDots:enable";
+ readonly FORMAT_DEFAULT_TYPE_MAP: "format:defaultTypeMap";
+ readonly FORMAT_NUMBER_DEFAULT_PATTERN: "format:number:defaultPattern";
+ readonly FORMAT_PERCENT_DEFAULT_PATTERN: "format:percent:defaultPattern";
+ readonly FORMAT_BYTES_DEFAULT_PATTERN: "format:bytes:defaultPattern";
+ readonly FORMAT_CURRENCY_DEFAULT_PATTERN: "format:currency:defaultPattern";
+ readonly FORMAT_NUMBER_DEFAULT_LOCALE: "format:number:defaultLocale";
+ readonly TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: "timepicker:refreshIntervalDefaults";
+ readonly TIMEPICKER_QUICK_RANGES: "timepicker:quickRanges";
+ readonly TIMEPICKER_TIME_DEFAULTS: "timepicker:timeDefaults";
+ readonly INDEXPATTERN_PLACEHOLDER: "indexPattern:placeholder";
+ readonly FILTERS_PINNED_BY_DEFAULT: "filters:pinnedByDefault";
+ readonly FILTERS_EDITOR_SUGGEST_VALUES: "filterEditor:suggestValues";
}
```
diff --git a/docs/images/Discover-ContextView.png b/docs/discover/images/Discover-ContextView.png
similarity index 100%
rename from docs/images/Discover-ContextView.png
rename to docs/discover/images/Discover-ContextView.png
diff --git a/docs/images/Discover-Start.png b/docs/discover/images/Discover-Start.png
similarity index 100%
rename from docs/images/Discover-Start.png
rename to docs/discover/images/Discover-Start.png
diff --git a/docs/images/Expanded-Document.png b/docs/discover/images/Expanded-Document.png
similarity index 100%
rename from docs/images/Expanded-Document.png
rename to docs/discover/images/Expanded-Document.png
diff --git a/docs/images/Histogram-Time.png b/docs/discover/images/Histogram-Time.png
similarity index 100%
rename from docs/images/Histogram-Time.png
rename to docs/discover/images/Histogram-Time.png
diff --git a/docs/images/NegativeFilter.jpg b/docs/discover/images/NegativeFilter.jpg
similarity index 100%
rename from docs/images/NegativeFilter.jpg
rename to docs/discover/images/NegativeFilter.jpg
diff --git a/docs/images/PositiveFilter.jpg b/docs/discover/images/PositiveFilter.jpg
similarity index 100%
rename from docs/images/PositiveFilter.jpg
rename to docs/discover/images/PositiveFilter.jpg
diff --git a/docs/images/Timepicker-View.png b/docs/discover/images/Timepicker-View.png
similarity index 100%
rename from docs/images/Timepicker-View.png
rename to docs/discover/images/Timepicker-View.png
diff --git a/docs/images/edit_filter_query_json.png b/docs/discover/images/edit_filter_query_json.png
similarity index 100%
rename from docs/images/edit_filter_query_json.png
rename to docs/discover/images/edit_filter_query_json.png
diff --git a/docs/images/filter-field.png b/docs/discover/images/filter-field.png
similarity index 100%
rename from docs/images/filter-field.png
rename to docs/discover/images/filter-field.png
diff --git a/docs/images/time-filter-bar.png b/docs/discover/images/time-filter-bar.png
similarity index 100%
rename from docs/images/time-filter-bar.png
rename to docs/discover/images/time-filter-bar.png
diff --git a/docs/images/time-filter-calendar.png b/docs/discover/images/time-filter-calendar.png
similarity index 100%
rename from docs/images/time-filter-calendar.png
rename to docs/discover/images/time-filter-calendar.png
diff --git a/docs/images/tutorial-dashboard.png b/docs/getting-started/images/tutorial-dashboard.png
similarity index 100%
rename from docs/images/tutorial-dashboard.png
rename to docs/getting-started/images/tutorial-dashboard.png
diff --git a/docs/images/tutorial-discover-2.png b/docs/getting-started/images/tutorial-discover-2.png
similarity index 100%
rename from docs/images/tutorial-discover-2.png
rename to docs/getting-started/images/tutorial-discover-2.png
diff --git a/docs/images/tutorial-discover-3.png b/docs/getting-started/images/tutorial-discover-3.png
similarity index 100%
rename from docs/images/tutorial-discover-3.png
rename to docs/getting-started/images/tutorial-discover-3.png
diff --git a/docs/images/tutorial-full-inspect1.png b/docs/getting-started/images/tutorial-full-inspect1.png
similarity index 100%
rename from docs/images/tutorial-full-inspect1.png
rename to docs/getting-started/images/tutorial-full-inspect1.png
diff --git a/docs/images/tutorial-pattern-1.png b/docs/getting-started/images/tutorial-pattern-1.png
similarity index 100%
rename from docs/images/tutorial-pattern-1.png
rename to docs/getting-started/images/tutorial-pattern-1.png
diff --git a/docs/images/tutorial-visualize-bar-1.5.png b/docs/getting-started/images/tutorial-visualize-bar-1.5.png
similarity index 100%
rename from docs/images/tutorial-visualize-bar-1.5.png
rename to docs/getting-started/images/tutorial-visualize-bar-1.5.png
diff --git a/docs/images/tutorial-visualize-map-2.png b/docs/getting-started/images/tutorial-visualize-map-2.png
similarity index 100%
rename from docs/images/tutorial-visualize-map-2.png
rename to docs/getting-started/images/tutorial-visualize-map-2.png
diff --git a/docs/images/tutorial-visualize-md-2.png b/docs/getting-started/images/tutorial-visualize-md-2.png
similarity index 100%
rename from docs/images/tutorial-visualize-md-2.png
rename to docs/getting-started/images/tutorial-visualize-md-2.png
diff --git a/docs/images/tutorial-visualize-pie-2.png b/docs/getting-started/images/tutorial-visualize-pie-2.png
similarity index 100%
rename from docs/images/tutorial-visualize-pie-2.png
rename to docs/getting-started/images/tutorial-visualize-pie-2.png
diff --git a/docs/images/tutorial-visualize-pie-3.png b/docs/getting-started/images/tutorial-visualize-pie-3.png
similarity index 100%
rename from docs/images/tutorial-visualize-pie-3.png
rename to docs/getting-started/images/tutorial-visualize-pie-3.png
diff --git a/docs/images/tutorial-visualize-wizard-step-1.png b/docs/getting-started/images/tutorial-visualize-wizard-step-1.png
similarity index 100%
rename from docs/images/tutorial-visualize-wizard-step-1.png
rename to docs/getting-started/images/tutorial-visualize-wizard-step-1.png
diff --git a/docs/images/AddFieldButton.jpg b/docs/images/AddFieldButton.jpg
deleted file mode 100644
index efd4f50e34a0b..0000000000000
Binary files a/docs/images/AddFieldButton.jpg and /dev/null differ
diff --git a/docs/images/CollapseButton.jpg b/docs/images/CollapseButton.jpg
deleted file mode 100644
index 38bb350d49746..0000000000000
Binary files a/docs/images/CollapseButton.jpg and /dev/null differ
diff --git a/docs/images/Dashboard_Resize_Menu.png b/docs/images/Dashboard_Resize_Menu.png
deleted file mode 100644
index 835d23afe40e9..0000000000000
Binary files a/docs/images/Dashboard_Resize_Menu.png and /dev/null differ
diff --git a/docs/images/Dashboard_visualization_data.png b/docs/images/Dashboard_visualization_data.png
deleted file mode 100644
index 9792fedf1a51a..0000000000000
Binary files a/docs/images/Dashboard_visualization_data.png and /dev/null differ
diff --git a/docs/images/Discover-ContextView-FilterMontage.png b/docs/images/Discover-ContextView-FilterMontage.png
deleted file mode 100644
index c990d314a6ba1..0000000000000
Binary files a/docs/images/Discover-ContextView-FilterMontage.png and /dev/null differ
diff --git a/docs/images/Discover-FieldStats.jpg b/docs/images/Discover-FieldStats.jpg
deleted file mode 100644
index 4092b0d7caafd..0000000000000
Binary files a/docs/images/Discover-FieldStats.jpg and /dev/null differ
diff --git a/docs/images/Discover-MoveColumn.jpg b/docs/images/Discover-MoveColumn.jpg
deleted file mode 100644
index 630f2a0f18dbe..0000000000000
Binary files a/docs/images/Discover-MoveColumn.jpg and /dev/null differ
diff --git a/docs/images/EditVis.png b/docs/images/EditVis.png
deleted file mode 100644
index 3013168200860..0000000000000
Binary files a/docs/images/EditVis.png and /dev/null differ
diff --git a/docs/images/ExistsButton.jpg b/docs/images/ExistsButton.jpg
deleted file mode 100644
index 0d4ede0101e73..0000000000000
Binary files a/docs/images/ExistsButton.jpg and /dev/null differ
diff --git a/docs/images/ExpandButton.jpg b/docs/images/ExpandButton.jpg
deleted file mode 100644
index 1ed389a25dd36..0000000000000
Binary files a/docs/images/ExpandButton.jpg and /dev/null differ
diff --git a/docs/images/NYCTA-Table.jpg b/docs/images/NYCTA-Table.jpg
deleted file mode 100644
index 6b4987ef4b437..0000000000000
Binary files a/docs/images/NYCTA-Table.jpg and /dev/null differ
diff --git a/docs/images/NewDashboard.png b/docs/images/NewDashboard.png
deleted file mode 100644
index 08e5159250134..0000000000000
Binary files a/docs/images/NewDashboard.png and /dev/null differ
diff --git a/docs/images/RemoveFieldButton.jpg b/docs/images/RemoveFieldButton.jpg
deleted file mode 100644
index a260dc3cff62e..0000000000000
Binary files a/docs/images/RemoveFieldButton.jpg and /dev/null differ
diff --git a/docs/images/Start-Page.png b/docs/images/Start-Page.png
deleted file mode 100644
index 706d4aafd75e2..0000000000000
Binary files a/docs/images/Start-Page.png and /dev/null differ
diff --git a/docs/images/TimeFilter.jpg b/docs/images/TimeFilter.jpg
deleted file mode 100644
index 1c8700bc05616..0000000000000
Binary files a/docs/images/TimeFilter.jpg and /dev/null differ
diff --git a/docs/images/VizEditor.jpg b/docs/images/VizEditor.jpg
deleted file mode 100644
index 8aabfe544a0cd..0000000000000
Binary files a/docs/images/VizEditor.jpg and /dev/null differ
diff --git a/docs/images/add-column-button.png b/docs/images/add-column-button.png
deleted file mode 100644
index 6f44d0facf41f..0000000000000
Binary files a/docs/images/add-column-button.png and /dev/null differ
diff --git a/docs/images/add_filter_field.png b/docs/images/add_filter_field.png
deleted file mode 100644
index 2052559cf5273..0000000000000
Binary files a/docs/images/add_filter_field.png and /dev/null differ
diff --git a/docs/images/add_filter_operator.png b/docs/images/add_filter_operator.png
deleted file mode 100644
index fd7d42a9d1b98..0000000000000
Binary files a/docs/images/add_filter_operator.png and /dev/null differ
diff --git a/docs/images/add_filter_value.png b/docs/images/add_filter_value.png
deleted file mode 100644
index d357c6e5a3013..0000000000000
Binary files a/docs/images/add_filter_value.png and /dev/null differ
diff --git a/docs/images/auto_format_after.png b/docs/images/auto_format_after.png
deleted file mode 100644
index 018e82951b64f..0000000000000
Binary files a/docs/images/auto_format_after.png and /dev/null differ
diff --git a/docs/images/auto_format_before.png b/docs/images/auto_format_before.png
deleted file mode 100644
index 2535aa1af5240..0000000000000
Binary files a/docs/images/auto_format_before.png and /dev/null differ
diff --git a/docs/images/auto_format_bulk.png b/docs/images/auto_format_bulk.png
deleted file mode 100644
index 92cb688473ab7..0000000000000
Binary files a/docs/images/auto_format_bulk.png and /dev/null differ
diff --git a/docs/images/autorefresh-intervals.png b/docs/images/autorefresh-intervals.png
deleted file mode 100644
index 49be46fefd4aa..0000000000000
Binary files a/docs/images/autorefresh-intervals.png and /dev/null differ
diff --git a/docs/images/autorefresh-pause.png b/docs/images/autorefresh-pause.png
deleted file mode 100644
index 5a83c4587c961..0000000000000
Binary files a/docs/images/autorefresh-pause.png and /dev/null differ
diff --git a/docs/images/autorefresh.png b/docs/images/autorefresh.png
deleted file mode 100644
index 9a6225b9007bd..0000000000000
Binary files a/docs/images/autorefresh.png and /dev/null differ
diff --git a/docs/images/bar-terms-agg.png b/docs/images/bar-terms-agg.png
deleted file mode 100644
index b0b62b9e53213..0000000000000
Binary files a/docs/images/bar-terms-agg.png and /dev/null differ
diff --git a/docs/images/bar-terms-subagg.png b/docs/images/bar-terms-subagg.png
deleted file mode 100644
index 37cf5486eff1e..0000000000000
Binary files a/docs/images/bar-terms-subagg.png and /dev/null differ
diff --git a/docs/images/canvas-align-elements.gif b/docs/images/canvas-align-elements.gif
deleted file mode 100644
index 0081308d68795..0000000000000
Binary files a/docs/images/canvas-align-elements.gif and /dev/null differ
diff --git a/docs/images/canvas-background-color-picker.gif b/docs/images/canvas-background-color-picker.gif
deleted file mode 100644
index bd22941b35f5d..0000000000000
Binary files a/docs/images/canvas-background-color-picker.gif and /dev/null differ
diff --git a/docs/images/canvas-click-drag-element.gif b/docs/images/canvas-click-drag-element.gif
deleted file mode 100644
index 34f4268caf6f5..0000000000000
Binary files a/docs/images/canvas-click-drag-element.gif and /dev/null differ
diff --git a/docs/images/canvas-distribute-elements.gif b/docs/images/canvas-distribute-elements.gif
deleted file mode 100644
index 685d76ba22e40..0000000000000
Binary files a/docs/images/canvas-distribute-elements.gif and /dev/null differ
diff --git a/docs/images/canvas-download-json.gif b/docs/images/canvas-download-json.gif
deleted file mode 100644
index c0c0025e508c1..0000000000000
Binary files a/docs/images/canvas-download-json.gif and /dev/null differ
diff --git a/docs/images/canvas-ecommerce.png b/docs/images/canvas-ecommerce.png
deleted file mode 100644
index 58c0612881341..0000000000000
Binary files a/docs/images/canvas-ecommerce.png and /dev/null differ
diff --git a/docs/images/canvas-element-order.gif b/docs/images/canvas-element-order.gif
deleted file mode 100644
index e2911367e7dfa..0000000000000
Binary files a/docs/images/canvas-element-order.gif and /dev/null differ
diff --git a/docs/images/canvas-embed_workpad.gif b/docs/images/canvas-embed_workpad.gif
deleted file mode 100644
index 97a79d775fe36..0000000000000
Binary files a/docs/images/canvas-embed_workpad.gif and /dev/null differ
diff --git a/docs/images/canvas-fullscreen.gif b/docs/images/canvas-fullscreen.gif
deleted file mode 100644
index 2eebd3b511000..0000000000000
Binary files a/docs/images/canvas-fullscreen.gif and /dev/null differ
diff --git a/docs/images/canvas-move-pixel.gif b/docs/images/canvas-move-pixel.gif
deleted file mode 100644
index 228f0f7b7e18c..0000000000000
Binary files a/docs/images/canvas-move-pixel.gif and /dev/null differ
diff --git a/docs/images/canvas-resize-element.gif b/docs/images/canvas-resize-element.gif
deleted file mode 100644
index d2d2ab06bbb42..0000000000000
Binary files a/docs/images/canvas-resize-element.gif and /dev/null differ
diff --git a/docs/images/canvas-zoom.gif b/docs/images/canvas-zoom.gif
deleted file mode 100644
index 584118d75a43f..0000000000000
Binary files a/docs/images/canvas-zoom.gif and /dev/null differ
diff --git a/docs/images/canvas_create_image.png b/docs/images/canvas_create_image.png
deleted file mode 100644
index 7b7c38102e4c9..0000000000000
Binary files a/docs/images/canvas_create_image.png and /dev/null differ
diff --git a/docs/images/canvas_map-time-filter.gif b/docs/images/canvas_map-time-filter.gif
deleted file mode 100644
index 301d7f4b44158..0000000000000
Binary files a/docs/images/canvas_map-time-filter.gif and /dev/null differ
diff --git a/docs/images/canvas_share_autoplay_480.gif b/docs/images/canvas_share_autoplay_480.gif
deleted file mode 100644
index 84a108e58d3dc..0000000000000
Binary files a/docs/images/canvas_share_autoplay_480.gif and /dev/null differ
diff --git a/docs/images/canvas_share_hidetoolbar_480.gif b/docs/images/canvas_share_hidetoolbar_480.gif
deleted file mode 100644
index 282783057776a..0000000000000
Binary files a/docs/images/canvas_share_hidetoolbar_480.gif and /dev/null differ
diff --git a/docs/images/canvas_workpad_3_page.png b/docs/images/canvas_workpad_3_page.png
deleted file mode 100644
index 9a60ed3d00f60..0000000000000
Binary files a/docs/images/canvas_workpad_3_page.png and /dev/null differ
diff --git a/docs/images/canvas_workpad_edit_style.png b/docs/images/canvas_workpad_edit_style.png
deleted file mode 100644
index d12ae2cd81b8f..0000000000000
Binary files a/docs/images/canvas_workpad_edit_style.png and /dev/null differ
diff --git a/docs/images/canvas_workpad_weblog.png b/docs/images/canvas_workpad_weblog.png
deleted file mode 100755
index 7b6ebee5c9554..0000000000000
Binary files a/docs/images/canvas_workpad_weblog.png and /dev/null differ
diff --git a/docs/images/controls/controls_options.png b/docs/images/controls/controls_options.png
deleted file mode 100644
index aab93d5cd4be0..0000000000000
Binary files a/docs/images/controls/controls_options.png and /dev/null differ
diff --git a/docs/images/controls/dropdown_control_editor.png b/docs/images/controls/dropdown_control_editor.png
deleted file mode 100644
index 36a360dcd275e..0000000000000
Binary files a/docs/images/controls/dropdown_control_editor.png and /dev/null differ
diff --git a/docs/images/controls/range_slider_editor.png b/docs/images/controls/range_slider_editor.png
deleted file mode 100644
index 8d6c5a68d1d24..0000000000000
Binary files a/docs/images/controls/range_slider_editor.png and /dev/null differ
diff --git a/docs/images/discover-compass.png b/docs/images/discover-compass.png
deleted file mode 100644
index 0e3c80ff75a74..0000000000000
Binary files a/docs/images/discover-compass.png and /dev/null differ
diff --git a/docs/images/edit_filter_query.png b/docs/images/edit_filter_query.png
deleted file mode 100644
index 367a2a8578b8b..0000000000000
Binary files a/docs/images/edit_filter_query.png and /dev/null differ
diff --git a/docs/images/filter-actions.png b/docs/images/filter-actions.png
deleted file mode 100644
index 92feef2f0dbbb..0000000000000
Binary files a/docs/images/filter-actions.png and /dev/null differ
diff --git a/docs/images/filter-allbuttons.png b/docs/images/filter-allbuttons.png
deleted file mode 100644
index 3d6951812daa7..0000000000000
Binary files a/docs/images/filter-allbuttons.png and /dev/null differ
diff --git a/docs/images/filter-sample.png b/docs/images/filter-sample.png
deleted file mode 100644
index 9d2540720a5a2..0000000000000
Binary files a/docs/images/filter-sample.png and /dev/null differ
diff --git a/docs/images/goal.png b/docs/images/goal.png
deleted file mode 100644
index 04f16e8cd3e74..0000000000000
Binary files a/docs/images/goal.png and /dev/null differ
diff --git a/docs/images/history.png b/docs/images/history.png
deleted file mode 100644
index 8e6674e1f2c69..0000000000000
Binary files a/docs/images/history.png and /dev/null differ
diff --git a/docs/images/labelbutton.png b/docs/images/labelbutton.png
deleted file mode 100644
index 287a588802384..0000000000000
Binary files a/docs/images/labelbutton.png and /dev/null differ
diff --git a/docs/images/lens_remove_layer.png b/docs/images/lens_remove_layer.png
deleted file mode 100644
index 4184e5b846870..0000000000000
Binary files a/docs/images/lens_remove_layer.png and /dev/null differ
diff --git a/docs/images/management-index-management.png b/docs/images/management-index-management.png
deleted file mode 100644
index 1b1ff9226147c..0000000000000
Binary files a/docs/images/management-index-management.png and /dev/null differ
diff --git a/docs/images/management-license.png b/docs/images/management-license.png
deleted file mode 100644
index 3347aec8632e4..0000000000000
Binary files a/docs/images/management-license.png and /dev/null differ
diff --git a/docs/images/management-upgrade-assistant-8.0.png b/docs/images/management-upgrade-assistant-8.0.png
deleted file mode 100644
index 4b37262414039..0000000000000
Binary files a/docs/images/management-upgrade-assistant-8.0.png and /dev/null differ
diff --git a/docs/images/management-watcher-buttons.png b/docs/images/management-watcher-buttons.png
deleted file mode 100644
index ce114ccf1bac9..0000000000000
Binary files a/docs/images/management-watcher-buttons.png and /dev/null differ
diff --git a/docs/images/management_rolled_dashboard.png b/docs/images/management_rolled_dashboard.png
deleted file mode 100755
index db731420fb96a..0000000000000
Binary files a/docs/images/management_rolled_dashboard.png and /dev/null differ
diff --git a/docs/images/management_rollups_visualization.png b/docs/images/management_rollups_visualization.png
deleted file mode 100755
index bba3b6e91a953..0000000000000
Binary files a/docs/images/management_rollups_visualization.png and /dev/null differ
diff --git a/docs/images/markdown-example.png b/docs/images/markdown-example.png
deleted file mode 100644
index 79daa1298883d..0000000000000
Binary files a/docs/images/markdown-example.png and /dev/null differ
diff --git a/docs/images/multiple_requests.png b/docs/images/multiple_requests.png
deleted file mode 100644
index e4fd010d54b4b..0000000000000
Binary files a/docs/images/multiple_requests.png and /dev/null differ
diff --git a/docs/images/regionmap.png b/docs/images/regionmap.png
deleted file mode 100644
index 97f2594e8bee6..0000000000000
Binary files a/docs/images/regionmap.png and /dev/null differ
diff --git a/docs/images/search-button.jpg b/docs/images/search-button.jpg
deleted file mode 100644
index b7787cac4bf6a..0000000000000
Binary files a/docs/images/search-button.jpg and /dev/null differ
diff --git a/docs/images/security_base_all.png b/docs/images/security_base_all.png
deleted file mode 100644
index 2aef42132ef21..0000000000000
Binary files a/docs/images/security_base_all.png and /dev/null differ
diff --git a/docs/images/share-short-link.png b/docs/images/share-short-link.png
deleted file mode 100644
index bf7f7782c4e2a..0000000000000
Binary files a/docs/images/share-short-link.png and /dev/null differ
diff --git a/docs/images/time-filter-absolute.jpg b/docs/images/time-filter-absolute.jpg
deleted file mode 100644
index bc54d57f0f737..0000000000000
Binary files a/docs/images/time-filter-absolute.jpg and /dev/null differ
diff --git a/docs/images/time-filter-relative.jpg b/docs/images/time-filter-relative.jpg
deleted file mode 100644
index 77beca3a3fd46..0000000000000
Binary files a/docs/images/time-filter-relative.jpg and /dev/null differ
diff --git a/docs/images/time-filter.jpg b/docs/images/time-filter.jpg
deleted file mode 100644
index e437f314d849d..0000000000000
Binary files a/docs/images/time-filter.jpg and /dev/null differ
diff --git a/docs/images/time-picker-step.jpg b/docs/images/time-picker-step.jpg
deleted file mode 100644
index 90c749776bb5d..0000000000000
Binary files a/docs/images/time-picker-step.jpg and /dev/null differ
diff --git a/docs/images/time-picker.jpg b/docs/images/time-picker.jpg
deleted file mode 100644
index 25830082d5919..0000000000000
Binary files a/docs/images/time-picker.jpg and /dev/null differ
diff --git a/docs/images/timelion-arg-help.jpg b/docs/images/timelion-arg-help.jpg
deleted file mode 100644
index 3e471c861d46b..0000000000000
Binary files a/docs/images/timelion-arg-help.jpg and /dev/null differ
diff --git a/docs/images/timelion-read-only-badge.png b/docs/images/timelion-read-only-badge.png
deleted file mode 100644
index 19ffbfed6335a..0000000000000
Binary files a/docs/images/timelion-read-only-badge.png and /dev/null differ
diff --git a/docs/images/timelion-save01.png b/docs/images/timelion-save01.png
deleted file mode 100644
index 47a33c2d36d43..0000000000000
Binary files a/docs/images/timelion-save01.png and /dev/null differ
diff --git a/docs/images/timelion-save02.png b/docs/images/timelion-save02.png
deleted file mode 100644
index 348b084ee5259..0000000000000
Binary files a/docs/images/timelion-save02.png and /dev/null differ
diff --git a/docs/images/tsvb-annotations.png b/docs/images/tsvb-annotations.png
deleted file mode 100644
index 22238db7e9e91..0000000000000
Binary files a/docs/images/tsvb-annotations.png and /dev/null differ
diff --git a/docs/images/tsvb-data-tab-derivative-example.png b/docs/images/tsvb-data-tab-derivative-example.png
deleted file mode 100644
index 66368baf1e16a..0000000000000
Binary files a/docs/images/tsvb-data-tab-derivative-example.png and /dev/null differ
diff --git a/docs/images/tsvb-data-tab-label.png b/docs/images/tsvb-data-tab-label.png
deleted file mode 100644
index 43d1fc64f4446..0000000000000
Binary files a/docs/images/tsvb-data-tab-label.png and /dev/null differ
diff --git a/docs/images/tsvb-data-tab-series-options-time-series.png b/docs/images/tsvb-data-tab-series-options-time-series.png
deleted file mode 100644
index 4c7ddadd38d95..0000000000000
Binary files a/docs/images/tsvb-data-tab-series-options-time-series.png and /dev/null differ
diff --git a/docs/images/tsvb-data-tab-series-options.png b/docs/images/tsvb-data-tab-series-options.png
deleted file mode 100644
index afadc3349bfe4..0000000000000
Binary files a/docs/images/tsvb-data-tab-series-options.png and /dev/null differ
diff --git a/docs/images/tutorial-full-inspect2.png b/docs/images/tutorial-full-inspect2.png
deleted file mode 100644
index 23c840f545ec3..0000000000000
Binary files a/docs/images/tutorial-full-inspect2.png and /dev/null differ
diff --git a/docs/images/tutorial-sample-discover-2.png b/docs/images/tutorial-sample-discover-2.png
deleted file mode 100644
index 4f4b2dc920ccb..0000000000000
Binary files a/docs/images/tutorial-sample-discover-2.png and /dev/null differ
diff --git a/docs/images/tutorial-sample-inspect2.png b/docs/images/tutorial-sample-inspect2.png
deleted file mode 100644
index b487d21e5cc02..0000000000000
Binary files a/docs/images/tutorial-sample-inspect2.png and /dev/null differ
diff --git a/docs/images/tutorial-visualize-pie-1.png b/docs/images/tutorial-visualize-pie-1.png
deleted file mode 100644
index 109829c01f28c..0000000000000
Binary files a/docs/images/tutorial-visualize-pie-1.png and /dev/null differ
diff --git a/docs/images/visualize-flow.png b/docs/images/visualize-flow.png
deleted file mode 100644
index bc00ff52a8d6e..0000000000000
Binary files a/docs/images/visualize-flow.png and /dev/null differ
diff --git a/docs/images/visualize-icon.png b/docs/images/visualize-icon.png
deleted file mode 100644
index af7ad18e9bf79..0000000000000
Binary files a/docs/images/visualize-icon.png and /dev/null differ
diff --git a/docs/images/visualize_coordinate_map_example.png b/docs/images/visualize_coordinate_map_example.png
deleted file mode 100644
index 24f03376adade..0000000000000
Binary files a/docs/images/visualize_coordinate_map_example.png and /dev/null differ
diff --git a/docs/images/visualize_region_map_example.png b/docs/images/visualize_region_map_example.png
deleted file mode 100644
index cf89e92625ece..0000000000000
Binary files a/docs/images/visualize_region_map_example.png and /dev/null differ
diff --git a/docs/images/viz-fit-bounds.png b/docs/images/viz-fit-bounds.png
deleted file mode 100644
index 9c0ddb89d7ddd..0000000000000
Binary files a/docs/images/viz-fit-bounds.png and /dev/null differ
diff --git a/docs/images/viz-lat-long-filter.png b/docs/images/viz-lat-long-filter.png
deleted file mode 100644
index 30c139b224565..0000000000000
Binary files a/docs/images/viz-lat-long-filter.png and /dev/null differ
diff --git a/docs/images/viz-zoom.png b/docs/images/viz-zoom.png
deleted file mode 100644
index 661e053130882..0000000000000
Binary files a/docs/images/viz-zoom.png and /dev/null differ
diff --git a/docs/images/follower_indices.png b/docs/management/alerting/images/follower_indices.png
similarity index 100%
rename from docs/images/follower_indices.png
rename to docs/management/alerting/images/follower_indices.png
diff --git a/docs/images/actions_icon.png b/docs/management/images/actions_icon.png
similarity index 100%
rename from docs/images/actions_icon.png
rename to docs/management/images/actions_icon.png
diff --git a/docs/images/add_remote_cluster.png b/docs/management/images/add_remote_cluster.png
similarity index 100%
rename from docs/images/add_remote_cluster.png
rename to docs/management/images/add_remote_cluster.png
diff --git a/docs/images/auto_follow_pattern.png b/docs/management/images/auto_follow_pattern.png
similarity index 100%
rename from docs/images/auto_follow_pattern.png
rename to docs/management/images/auto_follow_pattern.png
diff --git a/docs/images/colorformatter.png b/docs/management/images/colorformatter.png
similarity index 100%
rename from docs/images/colorformatter.png
rename to docs/management/images/colorformatter.png
diff --git a/docs/images/cross-cluster-replication-list-view.png b/docs/management/images/cross-cluster-replication-list-view.png
similarity index 100%
rename from docs/images/cross-cluster-replication-list-view.png
rename to docs/management/images/cross-cluster-replication-list-view.png
diff --git a/docs/images/index-lifecycle-policies-create.png b/docs/management/images/index-lifecycle-policies-create.png
similarity index 100%
rename from docs/images/index-lifecycle-policies-create.png
rename to docs/management/images/index-lifecycle-policies-create.png
diff --git a/docs/images/index_lifecycle_policies_options.png b/docs/management/images/index_lifecycle_policies_options.png
similarity index 100%
rename from docs/images/index_lifecycle_policies_options.png
rename to docs/management/images/index_lifecycle_policies_options.png
diff --git a/docs/images/index_management_add_policy.png b/docs/management/images/index_management_add_policy.png
similarity index 100%
rename from docs/images/index_management_add_policy.png
rename to docs/management/images/index_management_add_policy.png
diff --git a/docs/images/management-create-rollup-bar-chart.png b/docs/management/images/management-create-rollup-bar-chart.png
similarity index 100%
rename from docs/images/management-create-rollup-bar-chart.png
rename to docs/management/images/management-create-rollup-bar-chart.png
diff --git a/docs/images/management-index-patterns.png b/docs/management/images/management-index-patterns.png
similarity index 100%
rename from docs/images/management-index-patterns.png
rename to docs/management/images/management-index-patterns.png
diff --git a/docs/images/management-index-read-only-badge.png b/docs/management/images/management-index-read-only-badge.png
similarity index 100%
rename from docs/images/management-index-read-only-badge.png
rename to docs/management/images/management-index-read-only-badge.png
diff --git a/docs/images/management-index-templates-mappings.png b/docs/management/images/management-index-templates-mappings.png
similarity index 100%
rename from docs/images/management-index-templates-mappings.png
rename to docs/management/images/management-index-templates-mappings.png
diff --git a/docs/images/management-index-templates.png b/docs/management/images/management-index-templates.png
similarity index 100%
rename from docs/images/management-index-templates.png
rename to docs/management/images/management-index-templates.png
diff --git a/docs/management/images/management-license.png b/docs/management/images/management-license.png
new file mode 100644
index 0000000000000..8df9402939b2e
Binary files /dev/null and b/docs/management/images/management-license.png differ
diff --git a/docs/images/management-rollup-index-pattern.png b/docs/management/images/management-rollup-index-pattern.png
similarity index 100%
rename from docs/images/management-rollup-index-pattern.png
rename to docs/management/images/management-rollup-index-pattern.png
diff --git a/docs/images/management-saved-objects.png b/docs/management/images/management-saved-objects.png
similarity index 100%
rename from docs/images/management-saved-objects.png
rename to docs/management/images/management-saved-objects.png
diff --git a/docs/images/management-upgrade-assistant-9.0.png b/docs/management/images/management-upgrade-assistant-9.0.png
similarity index 100%
rename from docs/images/management-upgrade-assistant-9.0.png
rename to docs/management/images/management-upgrade-assistant-9.0.png
diff --git a/docs/images/management_create_rollup_job.png b/docs/management/images/management_create_rollup_job.png
similarity index 100%
rename from docs/images/management_create_rollup_job.png
rename to docs/management/images/management_create_rollup_job.png
diff --git a/docs/images/management_create_rollup_menu.png b/docs/management/images/management_create_rollup_menu.png
similarity index 100%
rename from docs/images/management_create_rollup_menu.png
rename to docs/management/images/management_create_rollup_menu.png
diff --git a/docs/images/management_index_create_wizard.png b/docs/management/images/management_index_create_wizard.png
similarity index 100%
rename from docs/images/management_index_create_wizard.png
rename to docs/management/images/management_index_create_wizard.png
diff --git a/docs/images/management_index_details.png b/docs/management/images/management_index_details.png
similarity index 100%
rename from docs/images/management_index_details.png
rename to docs/management/images/management_index_details.png
diff --git a/docs/images/management_index_labels.png b/docs/management/images/management_index_labels.png
similarity index 100%
rename from docs/images/management_index_labels.png
rename to docs/management/images/management_index_labels.png
diff --git a/docs/images/management_rollup_job_dashboard.png b/docs/management/images/management_rollup_job_dashboard.png
similarity index 100%
rename from docs/images/management_rollup_job_dashboard.png
rename to docs/management/images/management_rollup_job_dashboard.png
diff --git a/docs/images/management_rollup_job_details.png b/docs/management/images/management_rollup_job_details.png
similarity index 100%
rename from docs/images/management_rollup_job_details.png
rename to docs/management/images/management_rollup_job_details.png
diff --git a/docs/images/management_rollup_job_vis.png b/docs/management/images/management_rollup_job_vis.png
similarity index 100%
rename from docs/images/management_rollup_job_vis.png
rename to docs/management/images/management_rollup_job_vis.png
diff --git a/docs/images/management_rollup_list.png b/docs/management/images/management_rollup_list.png
similarity index 100%
rename from docs/images/management_rollup_list.png
rename to docs/management/images/management_rollup_list.png
diff --git a/docs/images/remote-clusters-list-view.png b/docs/management/images/remote-clusters-list-view.png
similarity index 100%
rename from docs/images/remote-clusters-list-view.png
rename to docs/management/images/remote-clusters-list-view.png
diff --git a/docs/images/settings-read-only-badge.png b/docs/management/images/settings-read-only-badge.png
similarity index 100%
rename from docs/images/settings-read-only-badge.png
rename to docs/management/images/settings-read-only-badge.png
diff --git a/docs/images/tutorial-ilm-custom-policy.png b/docs/management/images/tutorial-ilm-custom-policy.png
similarity index 100%
rename from docs/images/tutorial-ilm-custom-policy.png
rename to docs/management/images/tutorial-ilm-custom-policy.png
diff --git a/docs/images/tutorial-ilm-delete-phase-creation.png b/docs/management/images/tutorial-ilm-delete-phase-creation.png
similarity index 100%
rename from docs/images/tutorial-ilm-delete-phase-creation.png
rename to docs/management/images/tutorial-ilm-delete-phase-creation.png
diff --git a/docs/images/tutorial-ilm-delete-rollover.png b/docs/management/images/tutorial-ilm-delete-rollover.png
similarity index 100%
rename from docs/images/tutorial-ilm-delete-rollover.png
rename to docs/management/images/tutorial-ilm-delete-rollover.png
diff --git a/docs/images/tutorial-ilm-hotphaserollover-default.png b/docs/management/images/tutorial-ilm-hotphaserollover-default.png
similarity index 100%
rename from docs/images/tutorial-ilm-hotphaserollover-default.png
rename to docs/management/images/tutorial-ilm-hotphaserollover-default.png
diff --git a/docs/images/tutorial-ilm-modify-default-warm-phase-rollover.png b/docs/management/images/tutorial-ilm-modify-default-warm-phase-rollover.png
similarity index 100%
rename from docs/images/tutorial-ilm-modify-default-warm-phase-rollover.png
rename to docs/management/images/tutorial-ilm-modify-default-warm-phase-rollover.png
diff --git a/docs/management/managing-licenses.asciidoc b/docs/management/managing-licenses.asciidoc
index 6cd6657a0aaeb..99cfd12eeade9 100644
--- a/docs/management/managing-licenses.asciidoc
+++ b/docs/management/managing-licenses.asciidoc
@@ -1,28 +1,27 @@
[[managing-licenses]]
== License Management
-When you install the default distribution of {kib}, you receive a basic license
-with no expiration date. For the full list of free features that are included in
-the basic license, refer to https://www.elastic.co/subscriptions[the subscription page].
+When you install the default distribution of {kib}, you receive free features
+with no expiration date. For the full list of features, refer to
+{subscriptions}.
-If you want to try out the full set of platinum features, you can activate a
-30-day trial license. To view the
-status of your license, start a trial, or install a new license, open the menu, then go to *Stack Management > {es} > License Management*.
+If you want to try out the full set of features, you can activate a free 30-day
+trial. To view the status of your license, start a trial, or install a new
+license, open the menu, then go to *Stack Management > {es} > License Management*.
NOTE: You can start a trial only if your cluster has not already activated a
trial license for the current major product version. For example, if you have
already activated a trial for 6.0, you cannot start a new trial until
-7.0. You can, however, contact `info@elastic.co` to request an extended trial
-license.
+7.0. You can, however, request an extended trial at {extendtrial}.
When you activate a new license level, new features appear in *Stack Management*.
[role="screenshot"]
image::images/management-license.png[]
-At the end of the trial period, the platinum features operate in a
-<>. You can revert to a basic license,
-extend the trial, or purchase a subscription.
+At the end of the trial period, some features operate in a
+<>. You can revert to Basic, extend the trial,
+or purchase a subscription.
TIP: If {security-features} are enabled, unless you have a trial license,
you must configure Transport Layer Security (TLS) in {es}.
diff --git a/docs/images/add-data-fv.png b/docs/setup/images/add-data-fv.png
similarity index 100%
rename from docs/images/add-data-fv.png
rename to docs/setup/images/add-data-fv.png
diff --git a/docs/images/add-data-tutorials.png b/docs/setup/images/add-data-tutorials.png
similarity index 100%
rename from docs/images/add-data-tutorials.png
rename to docs/setup/images/add-data-tutorials.png
diff --git a/docs/images/data-viz-homepage.jpg b/docs/setup/images/data-viz-homepage.jpg
similarity index 100%
rename from docs/images/data-viz-homepage.jpg
rename to docs/setup/images/data-viz-homepage.jpg
diff --git a/docs/images/kibana-status-page-7_5_0.png b/docs/setup/images/kibana-status-page-7_5_0.png
similarity index 100%
rename from docs/images/kibana-status-page-7_5_0.png
rename to docs/setup/images/kibana-status-page-7_5_0.png
diff --git a/docs/uptime-guide/alerting.asciidoc b/docs/uptime-guide/alerting.asciidoc
deleted file mode 100644
index bf9e7693fc7a5..0000000000000
--- a/docs/uptime-guide/alerting.asciidoc
+++ /dev/null
@@ -1,33 +0,0 @@
-[role="xpack"]
-[[uptime-alerting]]
-
-=== Uptime alerting
-
-The Uptime app integrates with Kibana's {kibana-ref}/alerting-getting-started.html[alerting and actions]
-feature. It provides a set of built-in actions and Uptime specific threshold alerts for you to use
-and enables central management of all alerts from {kibana-ref}/management.html[Kibana Management].
-
-[role="screenshot"]
-image::images/create-alert.png[Create alert]
-
-[float]
-==== Monitor status alerts
-
-To receive alerts when a monitor goes down, use the alerting menu at the top of the
-overview page. Use a query in the alert flyout to determine which monitors to check
-with your alert. If you already have a query in the overview page search bar it will
-be carried over into this box.
-
-[role="screenshot"]
-image::images/monitor-status-alert.png[Create monitor status alert flyout]
-
-[float]
-==== TLS alerts
-
-Uptime also provides the ability to create an alert that will notify you when one or
-more of your monitors have a TLS certificate that will expire within some threshold,
-or when its age exceeds a limit. The values for these thresholds are configurable on
-the <>.
-
-[role="screenshot"]
-image::images/tls-alert.png[Create TLS alert flyout]
diff --git a/docs/uptime-guide/app-overview.asciidoc b/docs/uptime-guide/app-overview.asciidoc
deleted file mode 100644
index 692489a7ad311..0000000000000
--- a/docs/uptime-guide/app-overview.asciidoc
+++ /dev/null
@@ -1,70 +0,0 @@
-[role="xpack"]
-[[uptime-app]]
-== Uptime app
-
-The Uptime app in {kib} enables you to monitor the status of network endpoints via HTTP/S, TCP, and ICMP.
-You can explore endpoint status over time, drill down into specific monitors,
-and view a high-level snapshot of your environment at any point in time.
-
-[role="screenshot"]
-image::images/uptime-overview.png[Uptime app overview]
-
-[role="xpack"]
-[[uptime-app-overview]]
-=== Overview
-
-The Uptime overview helps you quickly identify and diagnose outages and
-other connectivity issues within your network or environment. You can use the date range
-selection that is global to the Uptime app, to highlight
-an absolute date range, or a relative one, similar to other areas of {kib}.
-
-[float]
-=== Filter bar
-
-The Filter bar enables you to quickly view specific groups of monitors, or even
-an individual monitor if you have defined many.
-
-This control allows you to use automated filter options, as well as input custom filter
-text to select specific monitors by field, URL, ID, and other attributes.
-
-[role="screenshot"]
-image::images/filter-bar.png[Filter bar]
-
-[float]
-=== Snapshot panel
-
-The Snapshot panel displays the overall
-status of the environment you're monitoring or a subset of those monitors.
-You can see the total number of detected monitors within the selected
-Uptime date range, along with the number of monitors
-in an `up` or `down` state, which is based on the last check reported by Heartbeat
-for each monitor.
-
-Next to the counts, there is a histogram displaying the change over time throughout the
-selected date range.
-
-[role="screenshot"]
-image::images/snapshot-view.png[Snapshot view]
-
-[float]
-=== Monitor list
-
-Information about individual monitors is displayed in the monitor list and provides a quick
-way to navigate to a more in-depth visualization for interesting hosts or endpoints.
-
-The information displayed includes the recent status of a host or endpoint, when the monitor was last checked, its
-ID and URL, and its IP address. There is also sparkline showing its check status over time.
-
-[role="screenshot"]
-image::images/monitor-list.png[Monitor list]
-
-[float]
-=== Observability integrations
-
-The Monitor list also contains a menu of available integrations. When Uptime detects Kubernetes or
-Docker related host information, it provides links to open the Metrics app or Logs app pre-filtered
-for this host. Additionally, to help you quickly determine if these solutions contain data relevant to you,
-this feature contains links to filter the other views on the host's IP address.
-
-[role="screenshot"]
-image::images/observability_integrations.png[Observability integrations]
diff --git a/docs/uptime-guide/certificates.asciidoc b/docs/uptime-guide/certificates.asciidoc
deleted file mode 100644
index 58db91aa080eb..0000000000000
--- a/docs/uptime-guide/certificates.asciidoc
+++ /dev/null
@@ -1,15 +0,0 @@
-[role="xpack"]
-[[uptime-certificates]]
-
-=== Certificates
-
-The certificates page enables you to visualize TLS certificate data in your indices. In addition to the
-common name, associated monitors, issuer information, and SHA fingerprints, Uptime also assigns a status
-derived from the threshold values in the <>.
-
-Several of the columns on this page are sortable. You can use the search bar at the top of the view
-to find values in most of the TLS-related fields in your Uptime indices. Additionally, using the `Alerts`
-dropdown at the top of the page you can create a TLS alert.
-
-[role="screenshot"]
-image::images/certificates-page.png[Certificates]
diff --git a/docs/uptime-guide/deployment-arch.asciidoc b/docs/uptime-guide/deployment-arch.asciidoc
deleted file mode 100644
index c1b2f596c6665..0000000000000
--- a/docs/uptime-guide/deployment-arch.asciidoc
+++ /dev/null
@@ -1,27 +0,0 @@
-[role="xpack"]
-[[uptime-deployment-arch]]
-== Deployment Architecture
-
-There are multiple ways to deploy Uptime and Heartbeat.
-Use the information in this section to determine the best deployment for you.
-A guiding principle is that when an outage takes down the service being monitored it should not also take down Heartbeat.
-You want Heartbeat to be functioning even when your service is not, so the guidelines here help you maximize this possibility.
-
-Heartbeat is commonly run as a centralized service within a data center.
-While it is possible to run it as a separate "sidecar" process paired with each process/container, we recommend against it.
-Running Heartbeat centrally ensures you will still be able to see monitoring data in the event of an overloaded, disconnected, or otherwise malfunctioning server.
-
-For further redundancy, you may want to deploy multiple Heartbeats across geographic and network boundaries to provide more data.
-To do so, specify Heartbeat's observer {heartbeat-ref}/configuration-observer-options.html[geo options].
-
-Some examples might be:
-
-* **A site served from a content delivery network (CDN) with points of presence (POPs) around the globe:**
-To check if your site is reachable via CDN POPS, you may want to have multiple Heartbeat instances at different data centers around the world.
-* **A service within a single data center that is accessed across multiple VPNs:**
-Set up one Heartbeat instance within the VPN the service operates from, and another within an additional VPN that users access the service from.
-Having both instances helps pinpoint network errors in the event of an outage.
-* **A single service running primarily in a US east coast data center, with a hot failover located in a US west coast data center:**
-In each data center, run a Heartbeat instance that checks both the local copy of the service and its counterpart across the country.
-Set up two monitors in each region, one for the local service and one for the remote service.
-In the event of a data center failure it will be immediately apparent if the service had a connectivity issue to the outside world or if the failure was only internal.
diff --git a/docs/uptime-guide/images/cert-exp.png b/docs/uptime-guide/images/cert-exp.png
deleted file mode 100644
index cd87668db96dd..0000000000000
Binary files a/docs/uptime-guide/images/cert-exp.png and /dev/null differ
diff --git a/docs/uptime-guide/images/certificates-page.png b/docs/uptime-guide/images/certificates-page.png
deleted file mode 100644
index 598aae982cd6a..0000000000000
Binary files a/docs/uptime-guide/images/certificates-page.png and /dev/null differ
diff --git a/docs/uptime-guide/images/check-history.png b/docs/uptime-guide/images/check-history.png
deleted file mode 100644
index aac5efd9b91d3..0000000000000
Binary files a/docs/uptime-guide/images/check-history.png and /dev/null differ
diff --git a/docs/uptime-guide/images/create-alert.png b/docs/uptime-guide/images/create-alert.png
deleted file mode 100644
index 54a0c400cad4c..0000000000000
Binary files a/docs/uptime-guide/images/create-alert.png and /dev/null differ
diff --git a/docs/uptime-guide/images/crosshair-example.png b/docs/uptime-guide/images/crosshair-example.png
deleted file mode 100644
index f9e89c4f622e0..0000000000000
Binary files a/docs/uptime-guide/images/crosshair-example.png and /dev/null differ
diff --git a/docs/uptime-guide/images/filter-bar.png b/docs/uptime-guide/images/filter-bar.png
deleted file mode 100644
index b7c424d3d0d91..0000000000000
Binary files a/docs/uptime-guide/images/filter-bar.png and /dev/null differ
diff --git a/docs/uptime-guide/images/indices.png b/docs/uptime-guide/images/indices.png
deleted file mode 100644
index 4090747b6726c..0000000000000
Binary files a/docs/uptime-guide/images/indices.png and /dev/null differ
diff --git a/docs/uptime-guide/images/monitor-charts.png b/docs/uptime-guide/images/monitor-charts.png
deleted file mode 100644
index 522f34662657e..0000000000000
Binary files a/docs/uptime-guide/images/monitor-charts.png and /dev/null differ
diff --git a/docs/uptime-guide/images/monitor-list.png b/docs/uptime-guide/images/monitor-list.png
deleted file mode 100644
index c9a8eccf01f6e..0000000000000
Binary files a/docs/uptime-guide/images/monitor-list.png and /dev/null differ
diff --git a/docs/uptime-guide/images/monitor-status-alert.png b/docs/uptime-guide/images/monitor-status-alert.png
deleted file mode 100644
index 847a0f58f02ce..0000000000000
Binary files a/docs/uptime-guide/images/monitor-status-alert.png and /dev/null differ
diff --git a/docs/uptime-guide/images/observability_integrations.png b/docs/uptime-guide/images/observability_integrations.png
deleted file mode 100644
index 3b23aa2dbd2a5..0000000000000
Binary files a/docs/uptime-guide/images/observability_integrations.png and /dev/null differ
diff --git a/docs/uptime-guide/images/settings.png b/docs/uptime-guide/images/settings.png
deleted file mode 100644
index d19b7f842ea68..0000000000000
Binary files a/docs/uptime-guide/images/settings.png and /dev/null differ
diff --git a/docs/uptime-guide/images/snapshot-view.png b/docs/uptime-guide/images/snapshot-view.png
deleted file mode 100644
index b6f07fb0721aa..0000000000000
Binary files a/docs/uptime-guide/images/snapshot-view.png and /dev/null differ
diff --git a/docs/uptime-guide/images/status-bar.png b/docs/uptime-guide/images/status-bar.png
deleted file mode 100644
index fd72e2b78c2a0..0000000000000
Binary files a/docs/uptime-guide/images/status-bar.png and /dev/null differ
diff --git a/docs/uptime-guide/images/tls-alert.png b/docs/uptime-guide/images/tls-alert.png
deleted file mode 100644
index 19efe07838903..0000000000000
Binary files a/docs/uptime-guide/images/tls-alert.png and /dev/null differ
diff --git a/docs/uptime-guide/images/uptime-multi-deployment.png b/docs/uptime-guide/images/uptime-multi-deployment.png
deleted file mode 100644
index 5440d91e48e23..0000000000000
Binary files a/docs/uptime-guide/images/uptime-multi-deployment.png and /dev/null differ
diff --git a/docs/uptime-guide/images/uptime-overview.png b/docs/uptime-guide/images/uptime-overview.png
deleted file mode 100644
index 25c88b2d14287..0000000000000
Binary files a/docs/uptime-guide/images/uptime-overview.png and /dev/null differ
diff --git a/docs/uptime-guide/images/uptime-setup.png b/docs/uptime-guide/images/uptime-setup.png
deleted file mode 100644
index 398125202fc4a..0000000000000
Binary files a/docs/uptime-guide/images/uptime-setup.png and /dev/null differ
diff --git a/docs/uptime-guide/images/uptime-simple-deployment.png b/docs/uptime-guide/images/uptime-simple-deployment.png
deleted file mode 100644
index f46dfdb2b8b86..0000000000000
Binary files a/docs/uptime-guide/images/uptime-simple-deployment.png and /dev/null differ
diff --git a/docs/uptime-guide/index.asciidoc b/docs/uptime-guide/index.asciidoc
deleted file mode 100644
index 01a93cb454ea9..0000000000000
--- a/docs/uptime-guide/index.asciidoc
+++ /dev/null
@@ -1,22 +0,0 @@
-
-include::{asciidoc-dir}/../../shared/versions/stack/{source_branch}.asciidoc[]
-include::{asciidoc-dir}/../../shared/attributes.asciidoc[]
-
-= Uptime monitoring guide
-
-include::overview.asciidoc[]
-
-include::install.asciidoc[]
-
-include::deployment-arch.asciidoc[]
-
-include::app-overview.asciidoc[]
-
-include::monitor.asciidoc[]
-
-include::settings.asciidoc[]
-
-include::certificates.asciidoc[]
-
-include::alerting.asciidoc[]
-
diff --git a/docs/uptime-guide/install.asciidoc b/docs/uptime-guide/install.asciidoc
deleted file mode 100644
index 05b9c6665562f..0000000000000
--- a/docs/uptime-guide/install.asciidoc
+++ /dev/null
@@ -1,74 +0,0 @@
-[[install-uptime]]
-== Install Uptime
-
-The easiest way to get started with Elastic Uptime is by using our hosted {es} Service on Elastic Cloud.
-The {es} Service is available on both AWS and GCP,
-and automatically configures {es} and {kib}.
-
-[float]
-=== Hosted Elasticsearch Service
-
-Skip managing your own {es} and {kib} instance by using our
-https://www.elastic.co/cloud/elasticsearch-service[hosted {es} Service] on
-Elastic Cloud.
-
-{ess-trial}[Try out the {es} Service for free],
-then jump straight to <>.
-
-[float]
-[[before-installation]]
-=== Install the stack yourself
-
-If you'd rather install the stack yourself,
-first see the https://www.elastic.co/support/matrix[Elastic Support Matrix] for information about supported operating systems and product compatibility.
-
-* <>
-* <>
-* <>
-
-[[install-elasticsearch]]
-=== Step 1: Install Elasticsearch
-
-Install an {es} cluster, start it up, and make sure it's running.
-
-. Verify that your system meets the
-https://www.elastic.co/support/matrix#matrix_jvm[minimum JVM requirements] for {es}.
-. {stack-gs}/get-started-elastic-stack.html#install-elasticsearch[Install Elasticsearch].
-. {stack-gs}/get-started-elastic-stack.html#_make_sure_elasticsearch_is_up_and_running[Make sure elasticsearch is up and running].
-
-[[install-kibana]]
-=== Step 2: Install Kibana
-
-Install {kib}, start it up, and open up the web interface:
-
-. {stack-gs}/get-started-elastic-stack.html#install-kibana[Install Kibana].
-. {stack-gs}/get-started-elastic-stack.html#_launch_the_kibana_web_interface[Launch the Kibana Web Interface].
-
-[[install-heartbeat]]
-=== Step 3: Install and configure Heartbeat
-
-Uptime requires the setup of monitors in Heartbeat.
-These monitors provide the data you'll be visualizing in the {kibana-ref}/xpack-uptime.html[Uptime app].
-
-For instructions on installing and configuring Heartbeat, see the *Setup Instructions* in {kib}.
-Additional information is available in {heartbeat-ref}/heartbeat-configuration.html[Configure Heartbeat].
-
-[role="screenshot"]
-image::images/uptime-setup.png[Installation instructions on the Uptime page in Kibana]
-
-[[setup-security]]
-=== Step 4: Set up Security
-
-Secure your installation by following the {heartbeat-ref}/securing-heartbeat.html[Secure Heartbeat] documentation.
-
-[float]
-==== Important considerations
-
-* Make sure you're using the same major versions of Heartbeat and {kib}.
-
-* Index patterns tell {kib} which {es} indices you want to explore.
-The Uptime app requires a +heartbeat-{major-version-only}*+ index pattern.
-If you have configured a different index pattern, you can use {ref}/indices-aliases.html[index aliases] to ensure data is recognized by the Uptime app.
-
-After you install and configure Heartbeat,
-the {kibana-ref}/xpack-uptime.html[Uptime app] is automatically populated with the Heartbeat monitors.
diff --git a/docs/uptime-guide/monitor.asciidoc b/docs/uptime-guide/monitor.asciidoc
deleted file mode 100644
index bb5d315cf63eb..0000000000000
--- a/docs/uptime-guide/monitor.asciidoc
+++ /dev/null
@@ -1,59 +0,0 @@
-[role="xpack"]
-[[uptime-monitor]]
-=== Monitor
-
-The Monitor page helps you gain insights into the performance
-of a specific network endpoint. A detailed visualization of
-the monitor's request duration over time, as well as the `up`/`down`
-status over time, is displayed. By configuring Machine Learning jobs
-on this page, you can also also detect anomalies in response time data.
-
-
-==== Status panel
-
-The Status panel displays a quick summary of the latest information
-regarding your monitor. You can view its latest status, click a link to
-visit the targeted URL, see its most recent request duration, and determine the
-amount of time that has elapsed since the last check.
-
-When two Heartbeat instances are configured in different geographic locations
-the map will show each location as a pinpoint on the map, along with the
-amount of time elapsed since data was last received from that location.
-
-[role="screenshot"]
-image::images/status-bar.png[Status bar]
-
-
-[float]
-==== Monitor charts
-
-The Monitor charts visualize information over the time specified in the
-date range. These charts help you gain insights into how quickly requests are being resolved
-by the targeted endpoint, and give you a sense of how frequently a host or endpoint
-was down in your selected timespan.
-
-[role="screenshot"]
-image::images/monitor-charts.png[Monitor charts]
-
-The Monitor duration chart displays request duration information for your monitor.
-The area surrounding the line is the range of request time for the corresponding
-bucket. The line is the average time. In the upper right hand of this panel
-you can enable Anomaly detection using Machine Learning. When response times change
-in an unexpected way the time range in which they occurred are highlighted with a color.
-
-The pings over time chart is a graphical representation of the check statuses over time.
-Hover over the charts to display crosshairs with specific numeric data.
-
-[role="screenshot"]
-image::images/crosshair-example.png[Chart crosshair]
-
-[float]
-==== Check history
-
-The Check history table lists the total count of this monitor's checks for the selected
-date range. To help find recent problems on a per-check basis, you can filter the checks
-by status and location. This table can help you gain some insight into more granular details
-about recent individual data points that Heartbeat is logging about your host or endpoint.
-
-[role="screenshot"]
-image::images/check-history.png[Check history view]
diff --git a/docs/uptime-guide/overview.asciidoc b/docs/uptime-guide/overview.asciidoc
deleted file mode 100644
index ab230b27f8cda..0000000000000
--- a/docs/uptime-guide/overview.asciidoc
+++ /dev/null
@@ -1,57 +0,0 @@
-[role="xpack"]
-[[uptime-overview]]
-== Elastic Uptime overview
-
-++++
-Overview
-++++
-
-Elastic Uptime enables you to monitor the availability and response times of applications and services in real time and to detect problems before they affect users.
-
-Elastic Uptime helps you to understand uptime and response time characteristics for your services and applications.
-It can be deployed both inside and outside your organization's network, so that you can analyze problems from multiple vantage points.
-
-Elastic Uptime uses these components: *Heartbeat*, *Elasticsearch* and *Kibana*.
-
-[float]
-=== Heartbeat
-
-{heartbeat-ref}/index.html[Heartbeat] is an open source data shipper that performs uptime monitoring.
-Elastic Uptime uses Heartbeat to collect monitoring data from your target applications and services, and ship it to Elasticsearch.
-
-[float]
-=== Elasticsearch
-
-{ref}/index.html[Elasticsearch] is a highly scalable, open source, search and analytics engine.
-Elasticsearch can store, search, and analyze large volumes of data in near real-time.
-Elastic Uptime uses Elasticsearch to store monitoring data from Heartbeat in Elasticsearch documents.
-
-[float]
-=== Kibana
-
-{kibana-ref}/index.html[Kibana] is an open source analytics and visualization platform designed to work with Elasticsearch.
-You can use Kibana to search, view, and interact with data stored in Elasticsearch.
-You can easily perform advanced data analysis and visualize your data in a variety of charts, tables, and maps.
-
-The {kibana-ref}/xpack-uptime.html[Elasticsearch Uptime app] in Kibana provides a dedicated user interface for viewing uptime data and identifying problem areas.
-
-[float]
-=== Example deployments
-// ++ I like the Infra/logging diagram which shows Metrics and Logging apps as separate components inside Kibana
-// ++ In diagram, should be Uptime app, not Uptime UI, possibly even Elastic Uptime? Also applies to Metrics/Logging/APM.
-// ++ Need more whitespace around components.
-
-In this simple deployment, a single instance of Heartbeat is deployed at a single monitoring location to monitor a single service.
-The Heartbeat instance sends the monitoring data to Elasticsearch.
-Then you can use the Uptime app in Kibana to view the data from Heartbeat and determine the status of the service.
-
-image::images/uptime-simple-deployment.png[Uptime simple deployment]
-
-In this deployment, two instances of Heartbeat are deployed at two different monitoring locations.
-Both instances monitor the same service.
-The Heartbeat instances send the monitoring data to Elasticsearch.
-As before, you can use the Uptime app in Kibana to view the Heartbeat data and determine the status of the service.
-When a failure occurs, the multiple monitoring locations enable you to pinpoint the area in which the failure has occurred.
-
-image::images/uptime-multi-deployment.png[Uptime multiple server deployment]
-
diff --git a/docs/uptime-guide/settings.asciidoc b/docs/uptime-guide/settings.asciidoc
deleted file mode 100644
index 59f9af631bfa7..0000000000000
--- a/docs/uptime-guide/settings.asciidoc
+++ /dev/null
@@ -1,51 +0,0 @@
-[role="xpack"]
-[[uptime-settings]]
-
-=== Settings
-
-The Uptime settings page lets you change which Heartbeat indices are displayed
-by the uptime app. Users must have the 'all' permission to modify items on this page.
-Uptime settings apply to the current space only. Use different settings in different
-spaces to segment different uptime use cases and domains.
-
-==== Indices
-
-Imagine your organization has one team for internal IT services, and another
-for public services. Each team operates independently and is only responsible for its
-own services. In this scenario, you might set up separate Heartbeat instances for each team,
-writing out to index patterns named `it-heartbeat-\*`, and `external-heartbeat-\*`. You would
-create separate roles and users for each in Elasticsearch, each with access to their own spaces,
-named `it` and `external` respectively. Within each space you would navigate to the settings page
-and set the correct index pattern to match only the indices that space is allowed to access.
-
-Note: The pattern set here only restricts what the Uptime app shows. Users may still be able
-to manually query Elasticsearch for data outside this pattern.
-
-[role="screenshot"]
-image::images/indices.png[Heartbeat indices]
-
-See the {kibana-ref}/uptime-security.html[Uptime security] and {heartbeat-ref}/securing-heartbeat.html[Heartbeat security]
-docs for more information.
-
-==== Certificate thresholds
-
-You can modify settings in this section to control how Uptime will visualize your TLS values in
-the <>. These settings also determine which certificates will be
-selected by any TLS alert you define.
-
-There are two fields, `age` and `expiration`. Use the `age` threshold to specify when Uptime should warn
-you about certificates that have been valid for too long. Use the `expiration` threshold to specify when Uptime should warn you
-about certificates that have approaching expiration dates.
-
-For example, a common security requirement is to make sure that none of your organization's TLS certificates have been
-valid for longer than one year. Modifying the `Age limit` field's value to 365 days will help you keep track of which
-certificates you may want to refresh.
-
-Likewise, to see which of your TLS certificates are close to expiring ahead of time, specify
-an `Expiration threshold` on this page. When the count of a certificate's remaining valid days falls
-below this threshold, Uptime will consider it in a warning state. When you define a TLS alert, you receive a
-notification from Uptime about the certificate.
-
-[role="screenshot"]
-image::images/cert-exp.png[Certification expiration thresholds]
-
diff --git a/docs/user/alerting/action-types/pagerduty.asciidoc b/docs/user/alerting/action-types/pagerduty.asciidoc
index 0468ab042e57e..5fd85a1045265 100644
--- a/docs/user/alerting/action-types/pagerduty.asciidoc
+++ b/docs/user/alerting/action-types/pagerduty.asciidoc
@@ -68,11 +68,11 @@ Then, select the *Integrations* tab and click the *New Integration* button.
* If you are creating a new service for your integration,
go to
https://support.pagerduty.com/docs/services-and-integrations#section-configuring-services-and-integrations[Configuring Services and Integrations]
-and follow the steps outlined in the *Create a New Service* section, selecting *Elastic* as the *Integration Type* in step 4.
+and follow the steps outlined in the *Create a New Service* section, selecting *Elastic Alerts* as the *Integration Type* in step 4.
Continue with the <> section once you have finished these steps.
. Enter an *Integration Name* in the format Elastic-service-name (for example, Elastic-Alerting or Kibana-APM-Alerting)
-and select Elastic from the *Integration Type* menu.
+and select *Elastic Alerts* from the *Integration Type* menu.
. Click *Add Integration* to save your new integration.
+
You will be redirected to the *Integrations* tab for your service. An Integration Key is generated on this screen.
diff --git a/docs/images/Dashboard_add_new_visualization.png b/docs/user/dashboard/images/Dashboard_add_new_visualization.png
similarity index 100%
rename from docs/images/Dashboard_add_new_visualization.png
rename to docs/user/dashboard/images/Dashboard_add_new_visualization.png
diff --git a/docs/images/Dashboard_add_visualization.png b/docs/user/dashboard/images/Dashboard_add_visualization.png
similarity index 100%
rename from docs/images/Dashboard_add_visualization.png
rename to docs/user/dashboard/images/Dashboard_add_visualization.png
diff --git a/docs/images/Dashboard_example.png b/docs/user/dashboard/images/Dashboard_example.png
similarity index 100%
rename from docs/images/Dashboard_example.png
rename to docs/user/dashboard/images/Dashboard_example.png
diff --git a/docs/images/Dashboard_inspect.png b/docs/user/dashboard/images/Dashboard_inspect.png
similarity index 100%
rename from docs/images/Dashboard_inspect.png
rename to docs/user/dashboard/images/Dashboard_inspect.png
diff --git a/docs/images/clone_panel.gif b/docs/user/dashboard/images/clone_panel.gif
similarity index 100%
rename from docs/images/clone_panel.gif
rename to docs/user/dashboard/images/clone_panel.gif
diff --git a/docs/images/dashboard-read-only-badge.png b/docs/user/dashboard/images/dashboard-read-only-badge.png
similarity index 100%
rename from docs/images/dashboard-read-only-badge.png
rename to docs/user/dashboard/images/dashboard-read-only-badge.png
diff --git a/docs/images/time_range_per_panel.gif b/docs/user/dashboard/images/time_range_per_panel.gif
similarity index 100%
rename from docs/images/time_range_per_panel.gif
rename to docs/user/dashboard/images/time_range_per_panel.gif
diff --git a/docs/images/intro-dashboard.png b/docs/user/introduction/images/intro-dashboard.png
similarity index 100%
rename from docs/images/intro-dashboard.png
rename to docs/user/introduction/images/intro-dashboard.png
diff --git a/docs/images/intro-data-tutorial.png b/docs/user/introduction/images/intro-data-tutorial.png
similarity index 100%
rename from docs/images/intro-data-tutorial.png
rename to docs/user/introduction/images/intro-data-tutorial.png
diff --git a/docs/images/intro-discover.png b/docs/user/introduction/images/intro-discover.png
similarity index 100%
rename from docs/images/intro-discover.png
rename to docs/user/introduction/images/intro-discover.png
diff --git a/docs/images/intro-kibana.png b/docs/user/introduction/images/intro-kibana.png
similarity index 100%
rename from docs/images/intro-kibana.png
rename to docs/user/introduction/images/intro-kibana.png
diff --git a/docs/images/intro-management.png b/docs/user/introduction/images/intro-management.png
similarity index 100%
rename from docs/images/intro-management.png
rename to docs/user/introduction/images/intro-management.png
diff --git a/docs/images/intro-spaces.jpg b/docs/user/introduction/images/intro-spaces.jpg
similarity index 100%
rename from docs/images/intro-spaces.jpg
rename to docs/user/introduction/images/intro-spaces.jpg
diff --git a/docs/images/monitoring-dashboard.png b/docs/user/monitoring/images/monitoring-dashboard.png
similarity index 100%
rename from docs/images/monitoring-dashboard.png
rename to docs/user/monitoring/images/monitoring-dashboard.png
diff --git a/docs/images/report-automate-csv.png b/docs/user/reporting/images/report-automate-csv.png
similarity index 100%
rename from docs/images/report-automate-csv.png
rename to docs/user/reporting/images/report-automate-csv.png
diff --git a/docs/images/report-automate-pdf.png b/docs/user/reporting/images/report-automate-pdf.png
similarity index 100%
rename from docs/images/report-automate-pdf.png
rename to docs/user/reporting/images/report-automate-pdf.png
diff --git a/docs/user/reporting/index.asciidoc b/docs/user/reporting/index.asciidoc
index 4123912b79237..6acdbbe3f0a99 100644
--- a/docs/user/reporting/index.asciidoc
+++ b/docs/user/reporting/index.asciidoc
@@ -19,7 +19,7 @@ image::user/reporting/images/share-button.png["Share"]
[float]
== Setup
-{reporting} is automatically enabled in {kib}. The first time {kib} runs, it extracts a custom build for the Chromium web browser, which
+{reporting} is automatically enabled in {kib}. It runs a custom build of the Chromium web browser, which
runs on the server in headless mode to load {kib} and capture the rendered {kib} charts as images.
Chromium is an open-source project not related to Elastic, but the Chromium binary for {kib} has been custom-built by Elastic to ensure it
diff --git a/docs/images/add-bucket.png b/docs/visualize/images/add-bucket.png
similarity index 100%
rename from docs/images/add-bucket.png
rename to docs/visualize/images/add-bucket.png
diff --git a/docs/images/apply-changes-button.png b/docs/visualize/images/apply-changes-button.png
similarity index 100%
rename from docs/images/apply-changes-button.png
rename to docs/visualize/images/apply-changes-button.png
diff --git a/docs/images/color-picker.png b/docs/visualize/images/color-picker.png
similarity index 100%
rename from docs/images/color-picker.png
rename to docs/visualize/images/color-picker.png
diff --git a/docs/images/dashboard-controls.png b/docs/visualize/images/dashboard-controls.png
similarity index 100%
rename from docs/images/dashboard-controls.png
rename to docs/visualize/images/dashboard-controls.png
diff --git a/docs/images/gauge.png b/docs/visualize/images/gauge.png
similarity index 100%
rename from docs/images/gauge.png
rename to docs/visualize/images/gauge.png
diff --git a/docs/images/lens_data_info.png b/docs/visualize/images/lens_data_info.png
similarity index 100%
rename from docs/images/lens_data_info.png
rename to docs/visualize/images/lens_data_info.png
diff --git a/docs/images/lens_drag_drop.gif b/docs/visualize/images/lens_drag_drop.gif
similarity index 100%
rename from docs/images/lens_drag_drop.gif
rename to docs/visualize/images/lens_drag_drop.gif
diff --git a/docs/images/lens_suggestions.gif b/docs/visualize/images/lens_suggestions.gif
similarity index 100%
rename from docs/images/lens_suggestions.gif
rename to docs/visualize/images/lens_suggestions.gif
diff --git a/docs/images/lens_tutorial_1.png b/docs/visualize/images/lens_tutorial_1.png
similarity index 100%
rename from docs/images/lens_tutorial_1.png
rename to docs/visualize/images/lens_tutorial_1.png
diff --git a/docs/images/lens_tutorial_2.png b/docs/visualize/images/lens_tutorial_2.png
similarity index 100%
rename from docs/images/lens_tutorial_2.png
rename to docs/visualize/images/lens_tutorial_2.png
diff --git a/docs/images/lens_tutorial_3.png b/docs/visualize/images/lens_tutorial_3.png
similarity index 100%
rename from docs/images/lens_tutorial_3.png
rename to docs/visualize/images/lens_tutorial_3.png
diff --git a/docs/images/lens_viz_types.png b/docs/visualize/images/lens_viz_types.png
similarity index 100%
rename from docs/images/lens_viz_types.png
rename to docs/visualize/images/lens_viz_types.png
diff --git a/docs/images/markdown_example_1.png b/docs/visualize/images/markdown_example_1.png
similarity index 100%
rename from docs/images/markdown_example_1.png
rename to docs/visualize/images/markdown_example_1.png
diff --git a/docs/images/markdown_example_2.png b/docs/visualize/images/markdown_example_2.png
similarity index 100%
rename from docs/images/markdown_example_2.png
rename to docs/visualize/images/markdown_example_2.png
diff --git a/docs/images/markdown_example_3.png b/docs/visualize/images/markdown_example_3.png
similarity index 100%
rename from docs/images/markdown_example_3.png
rename to docs/visualize/images/markdown_example_3.png
diff --git a/docs/images/markdown_example_4.png b/docs/visualize/images/markdown_example_4.png
similarity index 100%
rename from docs/images/markdown_example_4.png
rename to docs/visualize/images/markdown_example_4.png
diff --git a/docs/images/timelion-conditional01.png b/docs/visualize/images/timelion-conditional01.png
similarity index 100%
rename from docs/images/timelion-conditional01.png
rename to docs/visualize/images/timelion-conditional01.png
diff --git a/docs/images/timelion-conditional02.png b/docs/visualize/images/timelion-conditional02.png
similarity index 100%
rename from docs/images/timelion-conditional02.png
rename to docs/visualize/images/timelion-conditional02.png
diff --git a/docs/images/timelion-conditional03.png b/docs/visualize/images/timelion-conditional03.png
similarity index 100%
rename from docs/images/timelion-conditional03.png
rename to docs/visualize/images/timelion-conditional03.png
diff --git a/docs/images/timelion-conditional04.png b/docs/visualize/images/timelion-conditional04.png
similarity index 100%
rename from docs/images/timelion-conditional04.png
rename to docs/visualize/images/timelion-conditional04.png
diff --git a/docs/images/timelion-create01.png b/docs/visualize/images/timelion-create01.png
similarity index 100%
rename from docs/images/timelion-create01.png
rename to docs/visualize/images/timelion-create01.png
diff --git a/docs/images/timelion-create02.png b/docs/visualize/images/timelion-create02.png
similarity index 100%
rename from docs/images/timelion-create02.png
rename to docs/visualize/images/timelion-create02.png
diff --git a/docs/images/timelion-create03.png b/docs/visualize/images/timelion-create03.png
similarity index 100%
rename from docs/images/timelion-create03.png
rename to docs/visualize/images/timelion-create03.png
diff --git a/docs/images/timelion-customize01.png b/docs/visualize/images/timelion-customize01.png
similarity index 100%
rename from docs/images/timelion-customize01.png
rename to docs/visualize/images/timelion-customize01.png
diff --git a/docs/images/timelion-customize02.png b/docs/visualize/images/timelion-customize02.png
similarity index 100%
rename from docs/images/timelion-customize02.png
rename to docs/visualize/images/timelion-customize02.png
diff --git a/docs/images/timelion-customize03.png b/docs/visualize/images/timelion-customize03.png
similarity index 100%
rename from docs/images/timelion-customize03.png
rename to docs/visualize/images/timelion-customize03.png
diff --git a/docs/images/timelion-customize04.png b/docs/visualize/images/timelion-customize04.png
similarity index 100%
rename from docs/images/timelion-customize04.png
rename to docs/visualize/images/timelion-customize04.png
diff --git a/docs/images/timelion-math01.png b/docs/visualize/images/timelion-math01.png
similarity index 100%
rename from docs/images/timelion-math01.png
rename to docs/visualize/images/timelion-math01.png
diff --git a/docs/images/timelion-math02.png b/docs/visualize/images/timelion-math02.png
similarity index 100%
rename from docs/images/timelion-math02.png
rename to docs/visualize/images/timelion-math02.png
diff --git a/docs/images/timelion-math03.png b/docs/visualize/images/timelion-math03.png
similarity index 100%
rename from docs/images/timelion-math03.png
rename to docs/visualize/images/timelion-math03.png
diff --git a/docs/images/timelion-math04.png b/docs/visualize/images/timelion-math04.png
similarity index 100%
rename from docs/images/timelion-math04.png
rename to docs/visualize/images/timelion-math04.png
diff --git a/docs/images/timelion-math05.png b/docs/visualize/images/timelion-math05.png
similarity index 100%
rename from docs/images/timelion-math05.png
rename to docs/visualize/images/timelion-math05.png
diff --git a/docs/images/tsvb-gauge.png b/docs/visualize/images/tsvb-gauge.png
similarity index 100%
rename from docs/images/tsvb-gauge.png
rename to docs/visualize/images/tsvb-gauge.png
diff --git a/docs/images/tsvb-markdown.png b/docs/visualize/images/tsvb-markdown.png
similarity index 100%
rename from docs/images/tsvb-markdown.png
rename to docs/visualize/images/tsvb-markdown.png
diff --git a/docs/images/tsvb-metric.png b/docs/visualize/images/tsvb-metric.png
similarity index 100%
rename from docs/images/tsvb-metric.png
rename to docs/visualize/images/tsvb-metric.png
diff --git a/docs/images/tsvb-screenshot.png b/docs/visualize/images/tsvb-screenshot.png
similarity index 100%
rename from docs/images/tsvb-screenshot.png
rename to docs/visualize/images/tsvb-screenshot.png
diff --git a/docs/images/tsvb-table.png b/docs/visualize/images/tsvb-table.png
similarity index 100%
rename from docs/images/tsvb-table.png
rename to docs/visualize/images/tsvb-table.png
diff --git a/docs/images/tsvb-top-n.png b/docs/visualize/images/tsvb-top-n.png
similarity index 100%
rename from docs/images/tsvb-top-n.png
rename to docs/visualize/images/tsvb-top-n.png
diff --git a/docs/images/vega_lite_default.png b/docs/visualize/images/vega_lite_default.png
similarity index 100%
rename from docs/images/vega_lite_default.png
rename to docs/visualize/images/vega_lite_default.png
diff --git a/docs/images/visualize-date-histogram-split-1.png b/docs/visualize/images/visualize-date-histogram-split-1.png
similarity index 100%
rename from docs/images/visualize-date-histogram-split-1.png
rename to docs/visualize/images/visualize-date-histogram-split-1.png
diff --git a/docs/images/visualize-date-histogram-split-2.png b/docs/visualize/images/visualize-date-histogram-split-2.png
similarity index 100%
rename from docs/images/visualize-date-histogram-split-2.png
rename to docs/visualize/images/visualize-date-histogram-split-2.png
diff --git a/docs/images/visualize-date-histogram.png b/docs/visualize/images/visualize-date-histogram.png
similarity index 100%
rename from docs/images/visualize-date-histogram.png
rename to docs/visualize/images/visualize-date-histogram.png
diff --git a/docs/images/visualize-drag-reorder.png b/docs/visualize/images/visualize-drag-reorder.png
similarity index 100%
rename from docs/images/visualize-drag-reorder.png
rename to docs/visualize/images/visualize-drag-reorder.png
diff --git a/docs/images/visualize_heat_map_example.png b/docs/visualize/images/visualize_heat_map_example.png
similarity index 100%
rename from docs/images/visualize_heat_map_example.png
rename to docs/visualize/images/visualize_heat_map_example.png
diff --git a/examples/bfetch_explorer/kibana.json b/examples/bfetch_explorer/kibana.json
index 0039e9647bf83..f32cdfc13a1fe 100644
--- a/examples/bfetch_explorer/kibana.json
+++ b/examples/bfetch_explorer/kibana.json
@@ -5,5 +5,6 @@
"server": true,
"ui": true,
"requiredPlugins": ["bfetch", "developerExamples"],
- "optionalPlugins": []
+ "optionalPlugins": [],
+ "requiredBundles": ["kibanaReact"]
}
diff --git a/examples/dashboard_embeddable_examples/kibana.json b/examples/dashboard_embeddable_examples/kibana.json
index bb2ced569edb5..807229fad9dcf 100644
--- a/examples/dashboard_embeddable_examples/kibana.json
+++ b/examples/dashboard_embeddable_examples/kibana.json
@@ -5,5 +5,6 @@
"server": false,
"ui": true,
"requiredPlugins": ["embeddable", "embeddableExamples", "dashboard", "developerExamples"],
- "optionalPlugins": []
+ "optionalPlugins": [],
+ "requiredBundles": ["esUiShared"]
}
diff --git a/examples/embeddable_examples/common/book_saved_object_attributes.ts b/examples/embeddable_examples/common/book_saved_object_attributes.ts
new file mode 100644
index 0000000000000..62c08b7b81362
--- /dev/null
+++ b/examples/embeddable_examples/common/book_saved_object_attributes.ts
@@ -0,0 +1,28 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { SavedObjectAttributes } from '../../../src/core/types';
+
+export const BOOK_SAVED_OBJECT = 'book';
+
+export interface BookSavedObjectAttributes extends SavedObjectAttributes {
+ title: string;
+ author?: string;
+ readIt?: boolean;
+}
diff --git a/examples/embeddable_examples/common/index.ts b/examples/embeddable_examples/common/index.ts
index 726420fb9bdc3..55715113a12a2 100644
--- a/examples/embeddable_examples/common/index.ts
+++ b/examples/embeddable_examples/common/index.ts
@@ -18,3 +18,4 @@
*/
export { TodoSavedObjectAttributes } from './todo_saved_object_attributes';
+export { BookSavedObjectAttributes, BOOK_SAVED_OBJECT } from './book_saved_object_attributes';
diff --git a/examples/embeddable_examples/kibana.json b/examples/embeddable_examples/kibana.json
index 486c6322fad93..771c19cfdbd3d 100644
--- a/examples/embeddable_examples/kibana.json
+++ b/examples/embeddable_examples/kibana.json
@@ -4,7 +4,8 @@
"kibanaVersion": "kibana",
"server": true,
"ui": true,
- "requiredPlugins": ["embeddable"],
+ "requiredPlugins": ["embeddable", "uiActions"],
"optionalPlugins": [],
- "extraPublicDirs": ["public/todo", "public/hello_world", "public/todo/todo_ref_embeddable"]
+ "extraPublicDirs": ["public/todo", "public/hello_world", "public/todo/todo_ref_embeddable"],
+ "requiredBundles": ["kibanaReact"]
}
diff --git a/examples/embeddable_examples/public/book/book_component.tsx b/examples/embeddable_examples/public/book/book_component.tsx
new file mode 100644
index 0000000000000..064e13c131a0a
--- /dev/null
+++ b/examples/embeddable_examples/public/book/book_component.tsx
@@ -0,0 +1,90 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React from 'react';
+import { EuiFlexItem, EuiFlexGroup, EuiIcon } from '@elastic/eui';
+
+import { EuiText } from '@elastic/eui';
+import { EuiFlexGrid } from '@elastic/eui';
+import { withEmbeddableSubscription } from '../../../../src/plugins/embeddable/public';
+import { BookEmbeddableInput, BookEmbeddableOutput, BookEmbeddable } from './book_embeddable';
+
+interface Props {
+ input: BookEmbeddableInput;
+ output: BookEmbeddableOutput;
+ embeddable: BookEmbeddable;
+}
+
+function wrapSearchTerms(task?: string, search?: string) {
+ if (!search || !task) return task;
+ const parts = task.split(new RegExp(`(${search})`, 'g'));
+ return parts.map((part, i) =>
+ part === search ? (
+
+ {part}
+
+ ) : (
+ part
+ )
+ );
+}
+
+export function BookEmbeddableComponentInner({ input: { search }, output: { attributes } }: Props) {
+ const title = attributes?.title;
+ const author = attributes?.author;
+ const readIt = attributes?.readIt;
+
+ return (
+
+
+
+ {title ? (
+
+
+ {wrapSearchTerms(title, search)},
+
+
+ ) : null}
+ {author ? (
+
+
+ -{wrapSearchTerms(author, search)}
+
+
+ ) : null}
+ {readIt ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+ );
+}
+
+export const BookEmbeddableComponent = withEmbeddableSubscription<
+ BookEmbeddableInput,
+ BookEmbeddableOutput,
+ BookEmbeddable,
+ {}
+>(BookEmbeddableComponentInner);
diff --git a/examples/embeddable_examples/public/book/book_embeddable.tsx b/examples/embeddable_examples/public/book/book_embeddable.tsx
new file mode 100644
index 0000000000000..d49bd3280d97d
--- /dev/null
+++ b/examples/embeddable_examples/public/book/book_embeddable.tsx
@@ -0,0 +1,123 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { Subscription } from 'rxjs';
+import {
+ Embeddable,
+ EmbeddableInput,
+ IContainer,
+ EmbeddableOutput,
+ SavedObjectEmbeddableInput,
+ AttributeService,
+} from '../../../../src/plugins/embeddable/public';
+import { BookSavedObjectAttributes } from '../../common';
+import { BookEmbeddableComponent } from './book_component';
+
+export const BOOK_EMBEDDABLE = 'book';
+export type BookEmbeddableInput = BookByValueInput | BookByReferenceInput;
+export interface BookEmbeddableOutput extends EmbeddableOutput {
+ hasMatch: boolean;
+ attributes: BookSavedObjectAttributes;
+}
+
+interface BookInheritedInput extends EmbeddableInput {
+ search?: string;
+}
+
+export type BookByValueInput = { attributes: BookSavedObjectAttributes } & BookInheritedInput;
+export type BookByReferenceInput = SavedObjectEmbeddableInput & BookInheritedInput;
+
+/**
+ * Returns whether any attributes contain the search string. If search is empty, true is returned. If
+ * there are no savedAttributes, false is returned.
+ * @param search - the search string
+ * @param savedAttributes - the saved object attributes for the saved object with id `input.savedObjectId`
+ */
+function getHasMatch(search?: string, savedAttributes?: BookSavedObjectAttributes): boolean {
+ if (!search) return true;
+ if (!savedAttributes) return false;
+ return Boolean(
+ (savedAttributes.author && savedAttributes.author.match(search)) ||
+ (savedAttributes.title && savedAttributes.title.match(search))
+ );
+}
+
+export class BookEmbeddable extends Embeddable {
+ public readonly type = BOOK_EMBEDDABLE;
+ private subscription: Subscription;
+ private node?: HTMLElement;
+ private savedObjectId?: string;
+ private attributes?: BookSavedObjectAttributes;
+
+ constructor(
+ initialInput: BookEmbeddableInput,
+ private attributeService: AttributeService<
+ BookSavedObjectAttributes,
+ BookByValueInput,
+ BookByReferenceInput
+ >,
+ {
+ parent,
+ }: {
+ parent?: IContainer;
+ }
+ ) {
+ super(initialInput, {} as BookEmbeddableOutput, parent);
+
+ this.subscription = this.getInput$().subscribe(async () => {
+ const savedObjectId = (this.getInput() as BookByReferenceInput).savedObjectId;
+ const attributes = (this.getInput() as BookByValueInput).attributes;
+ if (this.attributes !== attributes || this.savedObjectId !== savedObjectId) {
+ this.savedObjectId = savedObjectId;
+ this.reload();
+ } else {
+ this.updateOutput({
+ attributes: this.attributes,
+ hasMatch: getHasMatch(this.input.search, this.attributes),
+ });
+ }
+ });
+ }
+
+ public render(node: HTMLElement) {
+ if (this.node) {
+ ReactDOM.unmountComponentAtNode(this.node);
+ }
+ this.node = node;
+ ReactDOM.render( , node);
+ }
+
+ public async reload() {
+ this.attributes = await this.attributeService.unwrapAttributes(this.input);
+
+ this.updateOutput({
+ attributes: this.attributes,
+ hasMatch: getHasMatch(this.input.search, this.attributes),
+ });
+ }
+
+ public destroy() {
+ super.destroy();
+ this.subscription.unsubscribe();
+ if (this.node) {
+ ReactDOM.unmountComponentAtNode(this.node);
+ }
+ }
+}
diff --git a/examples/embeddable_examples/public/book/book_embeddable_factory.tsx b/examples/embeddable_examples/public/book/book_embeddable_factory.tsx
new file mode 100644
index 0000000000000..f4a32fb498a2d
--- /dev/null
+++ b/examples/embeddable_examples/public/book/book_embeddable_factory.tsx
@@ -0,0 +1,127 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { BookSavedObjectAttributes, BOOK_SAVED_OBJECT } from '../../common';
+import { toMountPoint } from '../../../../src/plugins/kibana_react/public';
+import {
+ EmbeddableFactoryDefinition,
+ EmbeddableStart,
+ IContainer,
+ AttributeService,
+ EmbeddableFactory,
+} from '../../../../src/plugins/embeddable/public';
+import {
+ BookEmbeddable,
+ BOOK_EMBEDDABLE,
+ BookEmbeddableInput,
+ BookEmbeddableOutput,
+ BookByValueInput,
+ BookByReferenceInput,
+} from './book_embeddable';
+import { CreateEditBookComponent } from './create_edit_book_component';
+import { OverlayStart } from '../../../../src/core/public';
+
+interface StartServices {
+ getAttributeService: EmbeddableStart['getAttributeService'];
+ openModal: OverlayStart['openModal'];
+}
+
+export type BookEmbeddableFactory = EmbeddableFactory<
+ BookEmbeddableInput,
+ BookEmbeddableOutput,
+ BookEmbeddable,
+ BookSavedObjectAttributes
+>;
+
+export class BookEmbeddableFactoryDefinition
+ implements
+ EmbeddableFactoryDefinition<
+ BookEmbeddableInput,
+ BookEmbeddableOutput,
+ BookEmbeddable,
+ BookSavedObjectAttributes
+ > {
+ public readonly type = BOOK_EMBEDDABLE;
+ public savedObjectMetaData = {
+ name: 'Book',
+ includeFields: ['title', 'author', 'readIt'],
+ type: BOOK_SAVED_OBJECT,
+ getIconForSavedObject: () => 'pencil',
+ };
+
+ private attributeService?: AttributeService<
+ BookSavedObjectAttributes,
+ BookByValueInput,
+ BookByReferenceInput
+ >;
+
+ constructor(private getStartServices: () => Promise) {}
+
+ public async isEditable() {
+ return true;
+ }
+
+ public async create(input: BookEmbeddableInput, parent?: IContainer) {
+ return new BookEmbeddable(input, await this.getAttributeService(), {
+ parent,
+ });
+ }
+
+ public getDisplayName() {
+ return i18n.translate('embeddableExamples.book.displayName', {
+ defaultMessage: 'Book',
+ });
+ }
+
+ public async getExplicitInput(): Promise> {
+ const { openModal } = await this.getStartServices();
+ return new Promise>((resolve) => {
+ const onSave = async (attributes: BookSavedObjectAttributes, useRefType: boolean) => {
+ const wrappedAttributes = (await this.getAttributeService()).wrapAttributes(
+ attributes,
+ useRefType
+ );
+ resolve(wrappedAttributes);
+ };
+ const overlay = openModal(
+ toMountPoint(
+ {
+ onSave(attributes, useRefType);
+ overlay.close();
+ }}
+ />
+ )
+ );
+ });
+ }
+
+ private async getAttributeService() {
+ if (!this.attributeService) {
+ this.attributeService = await (await this.getStartServices()).getAttributeService<
+ BookSavedObjectAttributes,
+ BookByValueInput,
+ BookByReferenceInput
+ >(this.type);
+ }
+ return this.attributeService;
+ }
+}
diff --git a/examples/embeddable_examples/public/book/create_edit_book_component.tsx b/examples/embeddable_examples/public/book/create_edit_book_component.tsx
new file mode 100644
index 0000000000000..7e2d3cb9d88ab
--- /dev/null
+++ b/examples/embeddable_examples/public/book/create_edit_book_component.tsx
@@ -0,0 +1,88 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React, { useState } from 'react';
+import { EuiModalBody, EuiCheckbox } from '@elastic/eui';
+import { EuiFieldText } from '@elastic/eui';
+import { EuiButton } from '@elastic/eui';
+import { EuiModalFooter } from '@elastic/eui';
+import { EuiModalHeader } from '@elastic/eui';
+import { EuiFormRow } from '@elastic/eui';
+import { BookSavedObjectAttributes } from '../../common';
+
+export function CreateEditBookComponent({
+ savedObjectId,
+ attributes,
+ onSave,
+}: {
+ savedObjectId?: string;
+ attributes?: BookSavedObjectAttributes;
+ onSave: (attributes: BookSavedObjectAttributes, useRefType: boolean) => void;
+}) {
+ const [title, setTitle] = useState(attributes?.title ?? '');
+ const [author, setAuthor] = useState(attributes?.author ?? '');
+ const [readIt, setReadIt] = useState(attributes?.readIt ?? false);
+ return (
+
+
+ {`${savedObjectId ? 'Create new ' : 'Edit '}`}
+
+
+
+ setTitle(e.target.value)}
+ />
+
+
+ setAuthor(e.target.value)}
+ />
+
+
+ setReadIt(event.target.checked)}
+ />
+
+
+
+ onSave({ title, author, readIt }, false)}
+ >
+ {savedObjectId ? 'Unlink from library item' : 'Save and Return'}
+
+ onSave({ title, author, readIt }, true)}
+ >
+ {savedObjectId ? 'Update library item' : 'Save to library'}
+
+
+
+ );
+}
diff --git a/examples/embeddable_examples/public/book/edit_book_action.tsx b/examples/embeddable_examples/public/book/edit_book_action.tsx
new file mode 100644
index 0000000000000..222f70e0be60f
--- /dev/null
+++ b/examples/embeddable_examples/public/book/edit_book_action.tsx
@@ -0,0 +1,93 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React from 'react';
+import { OverlayStart } from 'kibana/public';
+import { i18n } from '@kbn/i18n';
+import { BookSavedObjectAttributes, BOOK_SAVED_OBJECT } from '../../common';
+import { createAction } from '../../../../src/plugins/ui_actions/public';
+import { toMountPoint } from '../../../../src/plugins/kibana_react/public';
+import {
+ ViewMode,
+ EmbeddableStart,
+ SavedObjectEmbeddableInput,
+} from '../../../../src/plugins/embeddable/public';
+import {
+ BookEmbeddable,
+ BOOK_EMBEDDABLE,
+ BookByReferenceInput,
+ BookByValueInput,
+} from './book_embeddable';
+import { CreateEditBookComponent } from './create_edit_book_component';
+
+interface StartServices {
+ openModal: OverlayStart['openModal'];
+ getAttributeService: EmbeddableStart['getAttributeService'];
+}
+
+interface ActionContext {
+ embeddable: BookEmbeddable;
+}
+
+export const ACTION_EDIT_BOOK = 'ACTION_EDIT_BOOK';
+
+export const createEditBookAction = (getStartServices: () => Promise) =>
+ createAction({
+ getDisplayName: () =>
+ i18n.translate('embeddableExamples.book.edit', { defaultMessage: 'Edit Book' }),
+ type: ACTION_EDIT_BOOK,
+ order: 100,
+ getIconType: () => 'documents',
+ isCompatible: async ({ embeddable }: ActionContext) => {
+ return (
+ embeddable.type === BOOK_EMBEDDABLE && embeddable.getInput().viewMode === ViewMode.EDIT
+ );
+ },
+ execute: async ({ embeddable }: ActionContext) => {
+ const { openModal, getAttributeService } = await getStartServices();
+ const attributeService = getAttributeService<
+ BookSavedObjectAttributes,
+ BookByValueInput,
+ BookByReferenceInput
+ >(BOOK_SAVED_OBJECT);
+ const onSave = async (attributes: BookSavedObjectAttributes, useRefType: boolean) => {
+ const newInput = await attributeService.wrapAttributes(attributes, useRefType, embeddable);
+ if (!useRefType && (embeddable.getInput() as SavedObjectEmbeddableInput).savedObjectId) {
+ // Remove the savedObejctId when un-linking
+ newInput.savedObjectId = null;
+ }
+ embeddable.updateInput(newInput);
+ if (useRefType) {
+ // Ensures that any duplicate embeddables also register the changes. This mirrors the behavior of going back and forth between apps
+ embeddable.getRoot().reload();
+ }
+ };
+ const overlay = openModal(
+ toMountPoint(
+ {
+ overlay.close();
+ onSave(attributes, useRefType);
+ }}
+ />
+ )
+ );
+ },
+ });
diff --git a/examples/embeddable_examples/public/book/index.ts b/examples/embeddable_examples/public/book/index.ts
new file mode 100644
index 0000000000000..46f44926e2152
--- /dev/null
+++ b/examples/embeddable_examples/public/book/index.ts
@@ -0,0 +1,21 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export * from './book_embeddable';
+export * from './book_embeddable_factory';
diff --git a/examples/embeddable_examples/public/create_sample_data.ts b/examples/embeddable_examples/public/create_sample_data.ts
index bd5ade18aa91e..d598c32a182fe 100644
--- a/examples/embeddable_examples/public/create_sample_data.ts
+++ b/examples/embeddable_examples/public/create_sample_data.ts
@@ -18,9 +18,9 @@
*/
import { SavedObjectsClientContract } from 'kibana/public';
-import { TodoSavedObjectAttributes } from '../common';
+import { TodoSavedObjectAttributes, BookSavedObjectAttributes, BOOK_SAVED_OBJECT } from '../common';
-export async function createSampleData(client: SavedObjectsClientContract) {
+export async function createSampleData(client: SavedObjectsClientContract, overwrite = true) {
await client.create(
'todo',
{
@@ -30,7 +30,20 @@ export async function createSampleData(client: SavedObjectsClientContract) {
},
{
id: 'sample-todo-saved-object',
- overwrite: true,
+ overwrite,
+ }
+ );
+
+ await client.create(
+ BOOK_SAVED_OBJECT,
+ {
+ title: 'Pillars of the Earth',
+ author: 'Ken Follett',
+ readIt: true,
+ },
+ {
+ id: 'sample-book-saved-object',
+ overwrite,
}
);
}
diff --git a/examples/embeddable_examples/public/index.ts b/examples/embeddable_examples/public/index.ts
index ec007f7c626f0..86f50f2b6e114 100644
--- a/examples/embeddable_examples/public/index.ts
+++ b/examples/embeddable_examples/public/index.ts
@@ -26,6 +26,8 @@ export {
export { ListContainer, LIST_CONTAINER, ListContainerFactory } from './list_container';
export { TODO_EMBEDDABLE, TodoEmbeddableFactory } from './todo';
+export { BOOK_EMBEDDABLE } from './book';
+
import { EmbeddableExamplesPlugin } from './plugin';
export {
diff --git a/examples/embeddable_examples/public/plugin.ts b/examples/embeddable_examples/public/plugin.ts
index d65ca1e8e7e8d..95f4f5b41e198 100644
--- a/examples/embeddable_examples/public/plugin.ts
+++ b/examples/embeddable_examples/public/plugin.ts
@@ -17,14 +17,19 @@
* under the License.
*/
-import { EmbeddableSetup, EmbeddableStart } from '../../../src/plugins/embeddable/public';
-import { CoreSetup, CoreStart, Plugin } from '../../../src/core/public';
import {
+ EmbeddableSetup,
+ EmbeddableStart,
+ CONTEXT_MENU_TRIGGER,
+} from '../../../src/plugins/embeddable/public';
+import { Plugin, CoreSetup, CoreStart } from '../../../src/core/public';
+import {
+ HelloWorldEmbeddableFactory,
HELLO_WORLD_EMBEDDABLE,
HelloWorldEmbeddableFactoryDefinition,
- HelloWorldEmbeddableFactory,
} from './hello_world';
import { TODO_EMBEDDABLE, TodoEmbeddableFactory, TodoEmbeddableFactoryDefinition } from './todo';
+
import {
MULTI_TASK_TODO_EMBEDDABLE,
MultiTaskTodoEmbeddableFactory,
@@ -46,9 +51,17 @@ import {
TodoRefEmbeddableFactory,
TodoRefEmbeddableFactoryDefinition,
} from './todo/todo_ref_embeddable_factory';
+import { ACTION_EDIT_BOOK, createEditBookAction } from './book/edit_book_action';
+import { BookEmbeddable, BOOK_EMBEDDABLE } from './book/book_embeddable';
+import {
+ BookEmbeddableFactory,
+ BookEmbeddableFactoryDefinition,
+} from './book/book_embeddable_factory';
+import { UiActionsStart } from '../../../src/plugins/ui_actions/public';
export interface EmbeddableExamplesSetupDependencies {
embeddable: EmbeddableSetup;
+ uiActions: UiActionsStart;
}
export interface EmbeddableExamplesStartDependencies {
@@ -62,6 +75,7 @@ interface ExampleEmbeddableFactories {
getListContainerEmbeddableFactory: () => ListContainerFactory;
getTodoEmbeddableFactory: () => TodoEmbeddableFactory;
getTodoRefEmbeddableFactory: () => TodoRefEmbeddableFactory;
+ getBookEmbeddableFactory: () => BookEmbeddableFactory;
}
export interface EmbeddableExamplesStart {
@@ -69,6 +83,12 @@ export interface EmbeddableExamplesStart {
factories: ExampleEmbeddableFactories;
}
+declare module '../../../src/plugins/ui_actions/public' {
+ export interface ActionContextMapping {
+ [ACTION_EDIT_BOOK]: { embeddable: BookEmbeddable };
+ }
+}
+
export class EmbeddableExamplesPlugin
implements
Plugin<
@@ -121,6 +141,20 @@ export class EmbeddableExamplesPlugin
getEmbeddableFactory: (await core.getStartServices())[1].embeddable.getEmbeddableFactory,
}))
);
+ this.exampleEmbeddableFactories.getBookEmbeddableFactory = deps.embeddable.registerEmbeddableFactory(
+ BOOK_EMBEDDABLE,
+ new BookEmbeddableFactoryDefinition(async () => ({
+ getAttributeService: (await core.getStartServices())[1].embeddable.getAttributeService,
+ openModal: (await core.getStartServices())[0].overlays.openModal,
+ }))
+ );
+
+ const editBookAction = createEditBookAction(async () => ({
+ getAttributeService: (await core.getStartServices())[1].embeddable.getAttributeService,
+ openModal: (await core.getStartServices())[0].overlays.openModal,
+ }));
+ deps.uiActions.registerAction(editBookAction);
+ deps.uiActions.attachAction(CONTEXT_MENU_TRIGGER, editBookAction.id);
}
public start(
diff --git a/test/functional/config.ie.js b/examples/embeddable_examples/server/book_saved_object.ts
similarity index 52%
rename from test/functional/config.ie.js
rename to examples/embeddable_examples/server/book_saved_object.ts
index bc47ce707003e..f0aca57f7925f 100644
--- a/test/functional/config.ie.js
+++ b/examples/embeddable_examples/server/book_saved_object.ts
@@ -17,36 +17,24 @@
* under the License.
*/
-export default async function ({ readConfigFile }) {
- const defaultConfig = await readConfigFile(require.resolve('./config'));
+import { SavedObjectsType } from 'kibana/server';
- return {
- ...defaultConfig.getAll(),
-
- browser: {
- type: 'ie',
- },
-
- junit: {
- reportName: 'Internet Explorer UI Functional Tests',
- },
-
- uiSettings: {
- defaults: {
- 'accessibility:disableAnimations': true,
- 'dateFormat:tz': 'UTC',
- 'state:storeInSessionStorage': true,
- 'notifications:lifetime:info': 10000,
+export const bookSavedObject: SavedObjectsType = {
+ name: 'book',
+ hidden: false,
+ namespaceType: 'agnostic',
+ mappings: {
+ properties: {
+ title: {
+ type: 'keyword',
+ },
+ author: {
+ type: 'keyword',
+ },
+ readIt: {
+ type: 'boolean',
},
},
-
- kbnTestServer: {
- ...defaultConfig.get('kbnTestServer'),
- serverArgs: [
- ...defaultConfig.get('kbnTestServer.serverArgs'),
- '--csp.strict=false',
- '--telemetry.optIn=false',
- ],
- },
- };
-}
+ },
+ migrations: {},
+};
diff --git a/examples/embeddable_examples/server/plugin.ts b/examples/embeddable_examples/server/plugin.ts
index d956b834d0d3c..1308ac9e0fc5e 100644
--- a/examples/embeddable_examples/server/plugin.ts
+++ b/examples/embeddable_examples/server/plugin.ts
@@ -19,10 +19,12 @@
import { Plugin, CoreSetup, CoreStart } from 'kibana/server';
import { todoSavedObject } from './todo_saved_object';
+import { bookSavedObject } from './book_saved_object';
export class EmbeddableExamplesPlugin implements Plugin {
public setup(core: CoreSetup) {
core.savedObjects.registerType(todoSavedObject);
+ core.savedObjects.registerType(bookSavedObject);
}
public start(core: CoreStart) {}
diff --git a/examples/embeddable_explorer/public/embeddable_panel_example.tsx b/examples/embeddable_explorer/public/embeddable_panel_example.tsx
index b2807f9a4c346..ca9675bb7f5a1 100644
--- a/examples/embeddable_explorer/public/embeddable_panel_example.tsx
+++ b/examples/embeddable_explorer/public/embeddable_panel_example.tsx
@@ -33,6 +33,7 @@ import { EmbeddableStart, IEmbeddable } from '../../../src/plugins/embeddable/pu
import {
HELLO_WORLD_EMBEDDABLE,
TODO_EMBEDDABLE,
+ BOOK_EMBEDDABLE,
MULTI_TASK_TODO_EMBEDDABLE,
SearchableListContainerFactory,
} from '../../embeddable_examples/public';
@@ -72,6 +73,35 @@ export function EmbeddablePanelExample({ embeddableServices, searchListContainer
tasks: ['Go to school', 'Watch planet earth', 'Read the encyclopedia'],
},
},
+ '4': {
+ type: BOOK_EMBEDDABLE,
+ explicitInput: {
+ id: '4',
+ savedObjectId: 'sample-book-saved-object',
+ },
+ },
+ '5': {
+ type: BOOK_EMBEDDABLE,
+ explicitInput: {
+ id: '5',
+ attributes: {
+ title: 'The Sympathizer',
+ author: 'Viet Thanh Nguyen',
+ readIt: true,
+ },
+ },
+ },
+ '6': {
+ type: BOOK_EMBEDDABLE,
+ explicitInput: {
+ id: '6',
+ attributes: {
+ title: 'The Hobbit',
+ author: 'J.R.R. Tolkien',
+ readIt: false,
+ },
+ },
+ },
},
};
diff --git a/examples/state_containers_examples/kibana.json b/examples/state_containers_examples/kibana.json
index 66da207cb4e77..58346af8f1d19 100644
--- a/examples/state_containers_examples/kibana.json
+++ b/examples/state_containers_examples/kibana.json
@@ -5,5 +5,6 @@
"server": true,
"ui": true,
"requiredPlugins": ["navigation", "data", "developerExamples"],
- "optionalPlugins": []
+ "optionalPlugins": [],
+ "requiredBundles": ["kibanaUtils", "kibanaReact"]
}
diff --git a/examples/ui_action_examples/kibana.json b/examples/ui_action_examples/kibana.json
index cd12442daf61c..0e0b6b6830b95 100644
--- a/examples/ui_action_examples/kibana.json
+++ b/examples/ui_action_examples/kibana.json
@@ -5,5 +5,6 @@
"server": false,
"ui": true,
"requiredPlugins": ["uiActions"],
- "optionalPlugins": []
+ "optionalPlugins": [],
+ "requiredBundles": ["kibanaReact"]
}
diff --git a/examples/ui_actions_explorer/kibana.json b/examples/ui_actions_explorer/kibana.json
index f57072e89b06d..0a55e60374710 100644
--- a/examples/ui_actions_explorer/kibana.json
+++ b/examples/ui_actions_explorer/kibana.json
@@ -5,5 +5,6 @@
"server": false,
"ui": true,
"requiredPlugins": ["uiActions", "uiActionsExamples", "developerExamples"],
- "optionalPlugins": []
+ "optionalPlugins": [],
+ "requiredBundles": ["kibanaReact"]
}
diff --git a/package.json b/package.json
index 1a497a2ec8b10..a45f240ce13af 100644
--- a/package.json
+++ b/package.json
@@ -127,7 +127,7 @@
"@elastic/datemath": "5.0.3",
"@elastic/elasticsearch": "7.8.0",
"@elastic/ems-client": "7.9.3",
- "@elastic/eui": "24.1.0",
+ "@elastic/eui": "26.3.1",
"@elastic/filesaver": "1.1.2",
"@elastic/good": "8.1.1-kibana2",
"@elastic/numeral": "^2.5.0",
diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/kibana.json b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/kibana.json
index 20c8046daa65e..33f53e336598d 100644
--- a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/kibana.json
+++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/kibana.json
@@ -1,4 +1,5 @@
{
"id": "bar",
- "ui": true
+ "ui": true,
+ "requiredBundles": ["foo"]
}
diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/legacy/_other_styles.scss b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/legacy/_other_styles.scss
new file mode 100644
index 0000000000000..2c1b9562b9567
--- /dev/null
+++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/legacy/_other_styles.scss
@@ -0,0 +1,3 @@
+p {
+ background-color: rebeccapurple;
+}
diff --git a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/legacy/styles.scss b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/legacy/styles.scss
index e71a2d485a2f8..1dc7bbe9daeb0 100644
--- a/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/legacy/styles.scss
+++ b/packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/public/legacy/styles.scss
@@ -1,3 +1,5 @@
+@import "./other_styles.scss";
+
body {
width: $globalStyleConstant;
background-image: url("ui/icon.svg");
diff --git a/packages/kbn-optimizer/src/cli.ts b/packages/kbn-optimizer/src/cli.ts
index 0916f12a7110d..9d3f4b88a258f 100644
--- a/packages/kbn-optimizer/src/cli.ts
+++ b/packages/kbn-optimizer/src/cli.ts
@@ -87,6 +87,11 @@ run(
throw createFlagError('expected --report-stats to have no value');
}
+ const filter = typeof flags.filter === 'string' ? [flags.filter] : flags.filter;
+ if (!Array.isArray(filter) || !filter.every((f) => typeof f === 'string')) {
+ throw createFlagError('expected --filter to be one or more strings');
+ }
+
const config = OptimizerConfig.create({
repoRoot: REPO_ROOT,
watch,
@@ -99,6 +104,7 @@ run(
extraPluginScanDirs,
inspectWorkers,
includeCoreBundle,
+ filter,
});
let update$ = runOptimizer(config);
@@ -128,12 +134,13 @@ run(
'inspect-workers',
'report-stats',
],
- string: ['workers', 'scan-dir'],
+ string: ['workers', 'scan-dir', 'filter'],
default: {
core: true,
examples: true,
cache: true,
'inspect-workers': true,
+ filter: [],
},
help: `
--watch run the optimizer in watch mode
@@ -142,6 +149,7 @@ run(
--profile profile the webpack builds and write stats.json files to build outputs
--no-core disable generating the core bundle
--no-cache disable the cache
+ --filter comma-separated list of bundle id filters, results from multiple flags are merged, * and ! are supported
--no-examples don't build the example plugins
--dist create bundles that are suitable for inclusion in the Kibana distributable
--scan-dir add a directory to the list of directories scanned for plugins (specify as many times as necessary)
diff --git a/packages/kbn-optimizer/src/common/bundle.test.ts b/packages/kbn-optimizer/src/common/bundle.test.ts
index b209bbca25ac4..6197a08485854 100644
--- a/packages/kbn-optimizer/src/common/bundle.test.ts
+++ b/packages/kbn-optimizer/src/common/bundle.test.ts
@@ -50,6 +50,7 @@ it('creates cache keys', () => {
"spec": Object {
"contextDir": "/foo/bar",
"id": "bar",
+ "manifestPath": undefined,
"outputDir": "/foo/bar/target",
"publicDirNames": Array [
"public",
@@ -85,6 +86,7 @@ it('parses bundles from JSON specs', () => {
},
"contextDir": "/foo/bar",
"id": "bar",
+ "manifestPath": undefined,
"outputDir": "/foo/bar/target",
"publicDirNames": Array [
"public",
diff --git a/packages/kbn-optimizer/src/common/bundle.ts b/packages/kbn-optimizer/src/common/bundle.ts
index 80af94c30f8da..a354da7a21521 100644
--- a/packages/kbn-optimizer/src/common/bundle.ts
+++ b/packages/kbn-optimizer/src/common/bundle.ts
@@ -18,6 +18,7 @@
*/
import Path from 'path';
+import Fs from 'fs';
import { BundleCache } from './bundle_cache';
import { UnknownVals } from './ts_helpers';
@@ -25,6 +26,11 @@ import { includes, ascending, entriesToObject } from './array_helpers';
const VALID_BUNDLE_TYPES = ['plugin' as const, 'entry' as const];
+const DEFAULT_IMPLICIT_BUNDLE_DEPS = ['core'];
+
+const isStringArray = (input: any): input is string[] =>
+ Array.isArray(input) && input.every((x) => typeof x === 'string');
+
export interface BundleSpec {
readonly type: typeof VALID_BUNDLE_TYPES[0];
/** Unique id for this bundle */
@@ -37,6 +43,8 @@ export interface BundleSpec {
readonly sourceRoot: string;
/** Absolute path to the directory where output should be written */
readonly outputDir: string;
+ /** Absolute path to a kibana.json manifest file, if omitted we assume there are not dependenices */
+ readonly manifestPath?: string;
}
export class Bundle {
@@ -56,6 +64,12 @@ export class Bundle {
public readonly sourceRoot: BundleSpec['sourceRoot'];
/** Absolute path to the output directory for this bundle */
public readonly outputDir: BundleSpec['outputDir'];
+ /**
+ * Absolute path to a manifest file with "requiredBundles" which will be
+ * used to allow bundleRefs from this bundle to the exports of another bundle.
+ * Every bundle mentioned in the `requiredBundles` must be built together.
+ */
+ public readonly manifestPath: BundleSpec['manifestPath'];
public readonly cache: BundleCache;
@@ -66,6 +80,7 @@ export class Bundle {
this.contextDir = spec.contextDir;
this.sourceRoot = spec.sourceRoot;
this.outputDir = spec.outputDir;
+ this.manifestPath = spec.manifestPath;
this.cache = new BundleCache(Path.resolve(this.outputDir, '.kbn-optimizer-cache'));
}
@@ -96,8 +111,54 @@ export class Bundle {
contextDir: this.contextDir,
sourceRoot: this.sourceRoot,
outputDir: this.outputDir,
+ manifestPath: this.manifestPath,
};
}
+
+ readBundleDeps(): { implicit: string[]; explicit: string[] } {
+ if (!this.manifestPath) {
+ return {
+ implicit: [...DEFAULT_IMPLICIT_BUNDLE_DEPS],
+ explicit: [],
+ };
+ }
+
+ let json: string;
+ try {
+ json = Fs.readFileSync(this.manifestPath, 'utf8');
+ } catch (error) {
+ if (error.code !== 'ENOENT') {
+ throw error;
+ }
+
+ json = '{}';
+ }
+
+ let parsedManifest: { requiredPlugins?: string[]; requiredBundles?: string[] };
+ try {
+ parsedManifest = JSON.parse(json);
+ } catch (error) {
+ throw new Error(
+ `unable to parse manifest at [${this.manifestPath}], error: [${error.message}]`
+ );
+ }
+
+ if (typeof parsedManifest === 'object' && parsedManifest) {
+ const explicit = parsedManifest.requiredBundles || [];
+ const implicit = [...DEFAULT_IMPLICIT_BUNDLE_DEPS, ...(parsedManifest.requiredPlugins || [])];
+
+ if (isStringArray(explicit) && isStringArray(implicit)) {
+ return {
+ explicit,
+ implicit,
+ };
+ }
+ }
+
+ throw new Error(
+ `Expected "requiredBundles" and "requiredPlugins" in manifest file [${this.manifestPath}] to be arrays of strings`
+ );
+ }
}
/**
@@ -152,6 +213,13 @@ export function parseBundles(json: string) {
throw new Error('`bundles[]` must have an absolute path `outputDir` property');
}
+ const { manifestPath } = spec;
+ if (manifestPath !== undefined) {
+ if (!(typeof manifestPath === 'string' && Path.isAbsolute(manifestPath))) {
+ throw new Error('`bundles[]` must have an absolute path `manifestPath` property');
+ }
+ }
+
return new Bundle({
type,
id,
@@ -159,6 +227,7 @@ export function parseBundles(json: string) {
contextDir,
sourceRoot,
outputDir,
+ manifestPath,
});
}
);
diff --git a/packages/kbn-optimizer/src/common/bundle_cache.ts b/packages/kbn-optimizer/src/common/bundle_cache.ts
index 5ae3e4c28a201..7607e270b5b4f 100644
--- a/packages/kbn-optimizer/src/common/bundle_cache.ts
+++ b/packages/kbn-optimizer/src/common/bundle_cache.ts
@@ -24,6 +24,7 @@ export interface State {
optimizerCacheKey?: unknown;
cacheKey?: unknown;
moduleCount?: number;
+ workUnits?: number;
files?: string[];
bundleRefExportIds?: string[];
}
@@ -96,6 +97,10 @@ export class BundleCache {
return this.get().cacheKey;
}
+ public getWorkUnits() {
+ return this.get().workUnits;
+ }
+
public getOptimizerCacheKey() {
return this.get().optimizerCacheKey;
}
diff --git a/packages/kbn-optimizer/src/common/bundle_refs.ts b/packages/kbn-optimizer/src/common/bundle_refs.ts
index a5c60f2031c0b..85731f32f8991 100644
--- a/packages/kbn-optimizer/src/common/bundle_refs.ts
+++ b/packages/kbn-optimizer/src/common/bundle_refs.ts
@@ -114,6 +114,10 @@ export class BundleRefs {
constructor(private readonly refs: BundleRef[]) {}
+ public forBundleIds(bundleIds: string[]) {
+ return this.refs.filter((r) => bundleIds.includes(r.bundleId));
+ }
+
public filterByExportIds(exportIds: string[]) {
return this.refs.filter((r) => exportIds.includes(r.exportId));
}
diff --git a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap
index 211cfac3806ad..c52873ab7ec20 100644
--- a/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap
+++ b/packages/kbn-optimizer/src/integration_tests/__snapshots__/basic_optimization.test.ts.snap
@@ -10,6 +10,7 @@ OptimizerConfig {
},
"contextDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar,
"id": "bar",
+ "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/kibana.json,
"outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/target/public,
"publicDirNames": Array [
"public",
@@ -24,6 +25,7 @@ OptimizerConfig {
},
"contextDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo,
"id": "foo",
+ "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/kibana.json,
"outputDir": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/target/public,
"publicDirNames": Array [
"public",
@@ -42,18 +44,21 @@ OptimizerConfig {
"extraPublicDirs": Array [],
"id": "bar",
"isUiPlugin": true,
+ "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/kibana.json,
},
Object {
"directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo,
"extraPublicDirs": Array [],
"id": "foo",
"isUiPlugin": true,
+ "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/kibana.json,
},
Object {
"directory": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/nested/baz,
"extraPublicDirs": Array [],
"id": "baz",
"isUiPlugin": false,
+ "manifestPath": /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/nested/baz/kibana.json,
},
],
"profileWebpack": false,
@@ -66,7 +71,7 @@ OptimizerConfig {
}
`;
-exports[`prepares assets for distribution: bar bundle 1`] = `"(function(modules){var installedModules={};function __webpack_require__(moduleId){if(installedModules[moduleId]){return installedModules[moduleId].exports}var module=installedModules[moduleId]={i:moduleId,l:false,exports:{}};modules[moduleId].call(module.exports,module,module.exports,__webpack_require__);module.l=true;return module.exports}__webpack_require__.m=modules;__webpack_require__.c=installedModules;__webpack_require__.d=function(exports,name,getter){if(!__webpack_require__.o(exports,name)){Object.defineProperty(exports,name,{enumerable:true,get:getter})}};__webpack_require__.r=function(exports){if(typeof Symbol!==\\"undefined\\"&&Symbol.toStringTag){Object.defineProperty(exports,Symbol.toStringTag,{value:\\"Module\\"})}Object.defineProperty(exports,\\"__esModule\\",{value:true})};__webpack_require__.t=function(value,mode){if(mode&1)value=__webpack_require__(value);if(mode&8)return value;if(mode&4&&typeof value===\\"object\\"&&value&&value.__esModule)return value;var ns=Object.create(null);__webpack_require__.r(ns);Object.defineProperty(ns,\\"default\\",{enumerable:true,value:value});if(mode&2&&typeof value!=\\"string\\")for(var key in value)__webpack_require__.d(ns,key,function(key){return value[key]}.bind(null,key));return ns};__webpack_require__.n=function(module){var getter=module&&module.__esModule?function getDefault(){return module[\\"default\\"]}:function getModuleExports(){return module};__webpack_require__.d(getter,\\"a\\",getter);return getter};__webpack_require__.o=function(object,property){return Object.prototype.hasOwnProperty.call(object,property)};__webpack_require__.p=\\"\\";return __webpack_require__(__webpack_require__.s=5)})([function(module,exports,__webpack_require__){\\"use strict\\";var isOldIE=function isOldIE(){var memo;return function memorize(){if(typeof memo===\\"undefined\\"){memo=Boolean(window&&document&&document.all&&!window.atob)}return memo}}();var getTarget=function getTarget(){var memo={};return function memorize(target){if(typeof memo[target]===\\"undefined\\"){var styleTarget=document.querySelector(target);if(window.HTMLIFrameElement&&styleTarget instanceof window.HTMLIFrameElement){try{styleTarget=styleTarget.contentDocument.head}catch(e){styleTarget=null}}memo[target]=styleTarget}return memo[target]}}();var stylesInDom=[];function getIndexByIdentifier(identifier){var result=-1;for(var i=0;i {
expect(foo.cache.getModuleCount()).toBe(6);
expect(foo.cache.getReferencedFiles()).toMatchInlineSnapshot(`
Array [
+ /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/kibana.json,
/packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/async_import.ts,
/packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/ext.ts,
/packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/foo/public/index.ts,
@@ -160,12 +161,17 @@ it('builds expected bundles, saves bundle counts to metadata', async () => {
Array [
/node_modules/css-loader/package.json,
/node_modules/style-loader/package.json,
+ /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/kibana.json,
/packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.scss,
/packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/index.ts,
+ /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/legacy/_other_styles.scss,
/packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/legacy/styles.scss,
/packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/plugins/bar/public/lib.ts,
/packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/legacy/ui/public/icon.svg,
+ /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/legacy/ui/public/styles/_globals_v7dark.scss,
+ /packages/kbn-optimizer/src/__fixtures__/__tmp__/mock_repo/src/legacy/ui/public/styles/_globals_v7light.scss,
/packages/kbn-optimizer/target/worker/entry_point_creator.js,
+ /packages/kbn-optimizer/target/worker/postcss.config.js,
/packages/kbn-ui-shared-deps/public_path_module_creator.js,
]
`);
diff --git a/packages/kbn-optimizer/src/log_optimizer_state.ts b/packages/kbn-optimizer/src/log_optimizer_state.ts
index 23767be610da4..20d98f74dbe86 100644
--- a/packages/kbn-optimizer/src/log_optimizer_state.ts
+++ b/packages/kbn-optimizer/src/log_optimizer_state.ts
@@ -54,12 +54,18 @@ export function logOptimizerState(log: ToolingLog, config: OptimizerConfig) {
if (event?.type === 'worker started') {
let moduleCount = 0;
+ let workUnits = 0;
for (const bundle of event.bundles) {
moduleCount += bundle.cache.getModuleCount() ?? NaN;
+ workUnits += bundle.cache.getWorkUnits() ?? NaN;
}
- const mcString = isFinite(moduleCount) ? String(moduleCount) : '?';
- const bcString = String(event.bundles.length);
- log.info(`starting worker [${bcString} bundles, ${mcString} modules]`);
+
+ log.info(
+ `starting worker [${event.bundles.length} ${
+ event.bundles.length === 1 ? 'bundle' : 'bundles'
+ }]`
+ );
+ log.debug(`modules [${moduleCount}] work units [${workUnits}]`);
}
if (state.phase === 'reallocating') {
diff --git a/packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.test.ts b/packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.test.ts
index ca50a49e26913..5443a88eb1a63 100644
--- a/packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.test.ts
+++ b/packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.test.ts
@@ -23,11 +23,11 @@ import { Bundle } from '../common';
import { assignBundlesToWorkers, Assignments } from './assign_bundles_to_workers';
-const hasModuleCount = (b: Bundle) => b.cache.getModuleCount() !== undefined;
-const noModuleCount = (b: Bundle) => b.cache.getModuleCount() === undefined;
+const hasWorkUnits = (b: Bundle) => b.cache.getWorkUnits() !== undefined;
+const noWorkUnits = (b: Bundle) => b.cache.getWorkUnits() === undefined;
const summarizeBundles = (w: Assignments) =>
[
- w.moduleCount ? `${w.moduleCount} known modules` : '',
+ w.workUnits ? `${w.workUnits} work units` : '',
w.newBundles ? `${w.newBundles} new bundles` : '',
]
.filter(Boolean)
@@ -42,15 +42,15 @@ const assertReturnVal = (workers: Assignments[]) => {
expect(workers).toBeInstanceOf(Array);
for (const worker of workers) {
expect(worker).toEqual({
- moduleCount: expect.any(Number),
+ workUnits: expect.any(Number),
newBundles: expect.any(Number),
bundles: expect.any(Array),
});
- expect(worker.bundles.filter(noModuleCount).length).toBe(worker.newBundles);
+ expect(worker.bundles.filter(noWorkUnits).length).toBe(worker.newBundles);
expect(
- worker.bundles.filter(hasModuleCount).reduce((sum, b) => sum + b.cache.getModuleCount()!, 0)
- ).toBe(worker.moduleCount);
+ worker.bundles.filter(hasWorkUnits).reduce((sum, b) => sum + b.cache.getWorkUnits()!, 0)
+ ).toBe(worker.workUnits);
}
};
@@ -76,7 +76,7 @@ const getBundles = ({
for (let i = 1; i <= withCounts; i++) {
const id = `foo${i}`;
const bundle = testBundle(id);
- bundle.cache.set({ moduleCount: i % 5 === 0 ? i * 10 : i });
+ bundle.cache.set({ workUnits: i % 5 === 0 ? i * 10 : i });
bundles.push(bundle);
}
@@ -95,8 +95,8 @@ it('creates less workers if maxWorkersCount is larger than bundle count', () =>
expect(workers.length).toBe(2);
expect(readConfigs(workers)).toMatchInlineSnapshot(`
Array [
- "worker 0 (1 known modules) => foo1",
- "worker 1 (2 known modules) => foo2",
+ "worker 0 (1 work units) => foo1",
+ "worker 1 (2 work units) => foo2",
]
`);
});
@@ -121,10 +121,10 @@ it('distributes bundles without module counts evenly after assigning modules wit
assertReturnVal(workers);
expect(readConfigs(workers)).toMatchInlineSnapshot(`
Array [
- "worker 0 (78 known modules, 3 new bundles) => foo5,foo11,foo8,foo6,foo2,foo1,bar9,bar5,bar1",
- "worker 1 (78 known modules, 3 new bundles) => foo16,foo14,foo13,foo12,foo9,foo7,foo4,foo3,bar8,bar4,bar0",
- "worker 2 (100 known modules, 2 new bundles) => foo10,bar7,bar3",
- "worker 3 (150 known modules, 2 new bundles) => foo15,bar6,bar2",
+ "worker 0 (78 work units, 3 new bundles) => foo5,foo11,foo8,foo6,foo2,foo1,bar9,bar5,bar1",
+ "worker 1 (78 work units, 3 new bundles) => foo16,foo14,foo13,foo12,foo9,foo7,foo4,foo3,bar8,bar4,bar0",
+ "worker 2 (100 work units, 2 new bundles) => foo10,bar7,bar3",
+ "worker 3 (150 work units, 2 new bundles) => foo15,bar6,bar2",
]
`);
});
@@ -135,8 +135,8 @@ it('distributes 2 bundles to workers evenly', () => {
assertReturnVal(workers);
expect(readConfigs(workers)).toMatchInlineSnapshot(`
Array [
- "worker 0 (1 known modules) => foo1",
- "worker 1 (2 known modules) => foo2",
+ "worker 0 (1 work units) => foo1",
+ "worker 1 (2 work units) => foo2",
]
`);
});
@@ -147,10 +147,10 @@ it('distributes 5 bundles to workers evenly', () => {
assertReturnVal(workers);
expect(readConfigs(workers)).toMatchInlineSnapshot(`
Array [
- "worker 0 (3 known modules) => foo2,foo1",
- "worker 1 (3 known modules) => foo3",
- "worker 2 (4 known modules) => foo4",
- "worker 3 (50 known modules) => foo5",
+ "worker 0 (3 work units) => foo2,foo1",
+ "worker 1 (3 work units) => foo3",
+ "worker 2 (4 work units) => foo4",
+ "worker 3 (50 work units) => foo5",
]
`);
});
@@ -161,10 +161,10 @@ it('distributes 10 bundles to workers evenly', () => {
assertReturnVal(workers);
expect(readConfigs(workers)).toMatchInlineSnapshot(`
Array [
- "worker 0 (20 known modules) => foo9,foo6,foo4,foo1",
- "worker 1 (20 known modules) => foo8,foo7,foo3,foo2",
- "worker 2 (50 known modules) => foo5",
- "worker 3 (100 known modules) => foo10",
+ "worker 0 (20 work units) => foo9,foo6,foo4,foo1",
+ "worker 1 (20 work units) => foo8,foo7,foo3,foo2",
+ "worker 2 (50 work units) => foo5",
+ "worker 3 (100 work units) => foo10",
]
`);
});
@@ -175,10 +175,10 @@ it('distributes 15 bundles to workers evenly', () => {
assertReturnVal(workers);
expect(readConfigs(workers)).toMatchInlineSnapshot(`
Array [
- "worker 0 (70 known modules) => foo14,foo13,foo12,foo11,foo9,foo6,foo4,foo1",
- "worker 1 (70 known modules) => foo5,foo8,foo7,foo3,foo2",
- "worker 2 (100 known modules) => foo10",
- "worker 3 (150 known modules) => foo15",
+ "worker 0 (70 work units) => foo14,foo13,foo12,foo11,foo9,foo6,foo4,foo1",
+ "worker 1 (70 work units) => foo5,foo8,foo7,foo3,foo2",
+ "worker 2 (100 work units) => foo10",
+ "worker 3 (150 work units) => foo15",
]
`);
});
@@ -189,10 +189,10 @@ it('distributes 20 bundles to workers evenly', () => {
assertReturnVal(workers);
expect(readConfigs(workers)).toMatchInlineSnapshot(`
Array [
- "worker 0 (153 known modules) => foo15,foo3",
- "worker 1 (153 known modules) => foo10,foo16,foo13,foo11,foo7,foo6",
- "worker 2 (154 known modules) => foo5,foo19,foo18,foo17,foo14,foo12,foo9,foo8,foo4,foo2,foo1",
- "worker 3 (200 known modules) => foo20",
+ "worker 0 (153 work units) => foo15,foo3",
+ "worker 1 (153 work units) => foo10,foo16,foo13,foo11,foo7,foo6",
+ "worker 2 (154 work units) => foo5,foo19,foo18,foo17,foo14,foo12,foo9,foo8,foo4,foo2,foo1",
+ "worker 3 (200 work units) => foo20",
]
`);
});
@@ -203,10 +203,10 @@ it('distributes 25 bundles to workers evenly', () => {
assertReturnVal(workers);
expect(readConfigs(workers)).toMatchInlineSnapshot(`
Array [
- "worker 0 (250 known modules) => foo20,foo17,foo13,foo9,foo8,foo2,foo1",
- "worker 1 (250 known modules) => foo15,foo23,foo22,foo18,foo16,foo11,foo7,foo3",
- "worker 2 (250 known modules) => foo10,foo5,foo24,foo21,foo19,foo14,foo12,foo6,foo4",
- "worker 3 (250 known modules) => foo25",
+ "worker 0 (250 work units) => foo20,foo17,foo13,foo9,foo8,foo2,foo1",
+ "worker 1 (250 work units) => foo15,foo23,foo22,foo18,foo16,foo11,foo7,foo3",
+ "worker 2 (250 work units) => foo10,foo5,foo24,foo21,foo19,foo14,foo12,foo6,foo4",
+ "worker 3 (250 work units) => foo25",
]
`);
});
@@ -217,10 +217,10 @@ it('distributes 30 bundles to workers evenly', () => {
assertReturnVal(workers);
expect(readConfigs(workers)).toMatchInlineSnapshot(`
Array [
- "worker 0 (352 known modules) => foo30,foo22,foo14,foo11,foo4,foo1",
- "worker 1 (352 known modules) => foo15,foo10,foo28,foo24,foo19,foo16,foo9,foo6",
- "worker 2 (353 known modules) => foo20,foo5,foo29,foo23,foo21,foo13,foo12,foo3,foo2",
- "worker 3 (353 known modules) => foo25,foo27,foo26,foo18,foo17,foo8,foo7",
+ "worker 0 (352 work units) => foo30,foo22,foo14,foo11,foo4,foo1",
+ "worker 1 (352 work units) => foo15,foo10,foo28,foo24,foo19,foo16,foo9,foo6",
+ "worker 2 (353 work units) => foo20,foo5,foo29,foo23,foo21,foo13,foo12,foo3,foo2",
+ "worker 3 (353 work units) => foo25,foo27,foo26,foo18,foo17,foo8,foo7",
]
`);
});
diff --git a/packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.ts b/packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.ts
index e1bcb22230bf9..44a3b21c5fd47 100644
--- a/packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.ts
+++ b/packages/kbn-optimizer/src/optimizer/assign_bundles_to_workers.ts
@@ -20,19 +20,18 @@
import { Bundle, descending, ascending } from '../common';
// helper types used inside getWorkerConfigs so we don't have
-// to calculate moduleCounts over and over
-
+// to calculate workUnits over and over
export interface Assignments {
- moduleCount: number;
+ workUnits: number;
newBundles: number;
bundles: Bundle[];
}
/** assign a wrapped bundle to a worker */
const assignBundle = (worker: Assignments, bundle: Bundle) => {
- const moduleCount = bundle.cache.getModuleCount();
- if (moduleCount !== undefined) {
- worker.moduleCount += moduleCount;
+ const workUnits = bundle.cache.getWorkUnits();
+ if (workUnits !== undefined) {
+ worker.workUnits += workUnits;
} else {
worker.newBundles += 1;
}
@@ -59,7 +58,7 @@ export function assignBundlesToWorkers(bundles: Bundle[], maxWorkerCount: number
const workers: Assignments[] = [];
for (let i = 0; i < workerCount; i++) {
workers.push({
- moduleCount: 0,
+ workUnits: 0,
newBundles: 0,
bundles: [],
});
@@ -67,18 +66,18 @@ export function assignBundlesToWorkers(bundles: Bundle[], maxWorkerCount: number
/**
* separate the bundles which do and don't have module
- * counts and sort them by [moduleCount, id]
+ * counts and sort them by [workUnits, id]
*/
const bundlesWithCountsDesc = bundles
- .filter((b) => b.cache.getModuleCount() !== undefined)
+ .filter((b) => b.cache.getWorkUnits() !== undefined)
.sort(
descending(
- (b) => b.cache.getModuleCount(),
+ (b) => b.cache.getWorkUnits(),
(b) => b.id
)
);
const bundlesWithoutModuleCounts = bundles
- .filter((b) => b.cache.getModuleCount() === undefined)
+ .filter((b) => b.cache.getWorkUnits() === undefined)
.sort(descending((b) => b.id));
/**
@@ -87,9 +86,9 @@ export function assignBundlesToWorkers(bundles: Bundle[], maxWorkerCount: number
* with module counts are assigned
*/
while (bundlesWithCountsDesc.length) {
- const [smallestWorker, nextSmallestWorker] = workers.sort(ascending((w) => w.moduleCount));
+ const [smallestWorker, nextSmallestWorker] = workers.sort(ascending((w) => w.workUnits));
- while (!nextSmallestWorker || smallestWorker.moduleCount <= nextSmallestWorker.moduleCount) {
+ while (!nextSmallestWorker || smallestWorker.workUnits <= nextSmallestWorker.workUnits) {
const bundle = bundlesWithCountsDesc.shift();
if (!bundle) {
@@ -104,7 +103,7 @@ export function assignBundlesToWorkers(bundles: Bundle[], maxWorkerCount: number
* assign bundles without module counts to workers round-robin
* starting with the smallest workers
*/
- workers.sort(ascending((w) => w.moduleCount));
+ workers.sort(ascending((w) => w.workUnits));
while (bundlesWithoutModuleCounts.length) {
for (const worker of workers) {
const bundle = bundlesWithoutModuleCounts.shift();
diff --git a/packages/kbn-optimizer/src/optimizer/filter_by_id.test.ts b/packages/kbn-optimizer/src/optimizer/filter_by_id.test.ts
new file mode 100644
index 0000000000000..3e848fe616b49
--- /dev/null
+++ b/packages/kbn-optimizer/src/optimizer/filter_by_id.test.ts
@@ -0,0 +1,72 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { filterById, HasId } from './filter_by_id';
+
+const bundles: HasId[] = [
+ { id: 'foo' },
+ { id: 'bar' },
+ { id: 'abc' },
+ { id: 'abcd' },
+ { id: 'abcde' },
+ { id: 'example_a' },
+];
+
+const print = (result: HasId[]) =>
+ result
+ .map((b) => b.id)
+ .sort((a, b) => a.localeCompare(b))
+ .join(', ');
+
+it('[] matches everything', () => {
+ expect(print(filterById([], bundles))).toMatchInlineSnapshot(
+ `"abc, abcd, abcde, bar, example_a, foo"`
+ );
+});
+
+it('* matches everything', () => {
+ expect(print(filterById(['*'], bundles))).toMatchInlineSnapshot(
+ `"abc, abcd, abcde, bar, example_a, foo"`
+ );
+});
+
+it('combines mutliple filters to select any bundle which is matched', () => {
+ expect(print(filterById(['foo', 'bar'], bundles))).toMatchInlineSnapshot(`"bar, foo"`);
+ expect(print(filterById(['bar', 'abc*'], bundles))).toMatchInlineSnapshot(
+ `"abc, abcd, abcde, bar"`
+ );
+});
+
+it('matches everything if any filter is *', () => {
+ expect(print(filterById(['*', '!abc*'], bundles))).toMatchInlineSnapshot(
+ `"abc, abcd, abcde, bar, example_a, foo"`
+ );
+});
+
+it('only matches bundles which are matched by an entire single filter', () => {
+ expect(print(filterById(['*,!abc*'], bundles))).toMatchInlineSnapshot(`"bar, example_a, foo"`);
+});
+
+it('handles purely positive filters', () => {
+ expect(print(filterById(['abc*'], bundles))).toMatchInlineSnapshot(`"abc, abcd, abcde"`);
+});
+
+it('handles purely negative filters', () => {
+ expect(print(filterById(['!abc*'], bundles))).toMatchInlineSnapshot(`"bar, example_a, foo"`);
+});
diff --git a/packages/kbn-optimizer/src/optimizer/filter_by_id.ts b/packages/kbn-optimizer/src/optimizer/filter_by_id.ts
new file mode 100644
index 0000000000000..ccf61a9efc880
--- /dev/null
+++ b/packages/kbn-optimizer/src/optimizer/filter_by_id.ts
@@ -0,0 +1,48 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export interface HasId {
+ id: string;
+}
+
+function parseFilter(filter: string) {
+ const positive: RegExp[] = [];
+ const negative: RegExp[] = [];
+
+ for (const segment of filter.split(',')) {
+ let trimmed = segment.trim();
+ let list = positive;
+
+ if (trimmed.startsWith('!')) {
+ trimmed = trimmed.slice(1);
+ list = negative;
+ }
+
+ list.push(new RegExp(`^${trimmed.split('*').join('.*')}$`));
+ }
+
+ return (bundle: HasId) =>
+ (!positive.length || positive.some((p) => p.test(bundle.id))) &&
+ (!negative.length || !negative.some((p) => p.test(bundle.id)));
+}
+
+export function filterById(filterStrings: string[], bundles: T[]) {
+ const filters = filterStrings.map(parseFilter);
+ return bundles.filter((b) => !filters.length || filters.some((f) => f(b)));
+}
diff --git a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts
index bbd3ddc11f448..a70cfc759dd55 100644
--- a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts
+++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.test.ts
@@ -32,18 +32,21 @@ it('returns a bundle for core and each plugin', () => {
id: 'foo',
isUiPlugin: true,
extraPublicDirs: [],
+ manifestPath: '/repo/plugins/foo/kibana.json',
},
{
directory: '/repo/plugins/bar',
id: 'bar',
isUiPlugin: false,
extraPublicDirs: [],
+ manifestPath: '/repo/plugins/bar/kibana.json',
},
{
directory: '/outside/of/repo/plugins/baz',
id: 'baz',
isUiPlugin: true,
extraPublicDirs: [],
+ manifestPath: '/outside/of/repo/plugins/baz/kibana.json',
},
],
'/repo'
@@ -53,6 +56,7 @@ it('returns a bundle for core and each plugin', () => {
Object {
"contextDir": /plugins/foo,
"id": "foo",
+ "manifestPath": /plugins/foo/kibana.json,
"outputDir": /plugins/foo/target/public,
"publicDirNames": Array [
"public",
@@ -63,6 +67,7 @@ it('returns a bundle for core and each plugin', () => {
Object {
"contextDir": "/outside/of/repo/plugins/baz",
"id": "baz",
+ "manifestPath": "/outside/of/repo/plugins/baz/kibana.json",
"outputDir": "/outside/of/repo/plugins/baz/target/public",
"publicDirNames": Array [
"public",
diff --git a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts
index 2635289088725..04ab992addeec 100644
--- a/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts
+++ b/packages/kbn-optimizer/src/optimizer/get_plugin_bundles.ts
@@ -35,6 +35,7 @@ export function getPluginBundles(plugins: KibanaPlatformPlugin[], repoRoot: stri
sourceRoot: repoRoot,
contextDir: p.directory,
outputDir: Path.resolve(p.directory, 'target/public'),
+ manifestPath: p.manifestPath,
})
);
}
diff --git a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.test.ts b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.test.ts
index f7b457ca42c6d..06fffc953f58b 100644
--- a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.test.ts
+++ b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.test.ts
@@ -40,24 +40,28 @@ it('parses kibana.json files of plugins found in pluginDirs', () => {
"extraPublicDirs": Array [],
"id": "bar",
"isUiPlugin": true,
+ "manifestPath": /packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/bar/kibana.json,
},
Object {
"directory": /packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo,
"extraPublicDirs": Array [],
"id": "foo",
"isUiPlugin": true,
+ "manifestPath": /packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/foo/kibana.json,
},
Object {
"directory": /packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/nested/baz,
"extraPublicDirs": Array [],
"id": "baz",
"isUiPlugin": false,
+ "manifestPath": /packages/kbn-optimizer/src/__fixtures__/mock_repo/plugins/nested/baz/kibana.json,
},
Object {
"directory": /packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz,
"extraPublicDirs": Array [],
"id": "test_baz",
"isUiPlugin": false,
+ "manifestPath": /packages/kbn-optimizer/src/__fixtures__/mock_repo/test_plugins/test_baz/kibana.json,
},
]
`);
diff --git a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts
index 83637691004f4..b489c53be47b9 100644
--- a/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts
+++ b/packages/kbn-optimizer/src/optimizer/kibana_platform_plugins.ts
@@ -24,6 +24,7 @@ import loadJsonFile from 'load-json-file';
export interface KibanaPlatformPlugin {
readonly directory: string;
+ readonly manifestPath: string;
readonly id: string;
readonly isUiPlugin: boolean;
readonly extraPublicDirs: string[];
@@ -92,6 +93,7 @@ function readKibanaPlatformPlugin(manifestPath: string): KibanaPlatformPlugin {
return {
directory: Path.dirname(manifestPath),
+ manifestPath,
id: manifest.id,
isUiPlugin: !!manifest.ui,
extraPublicDirs: extraPublicDirs || [],
diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts
index 5b46d67479fd5..f97646e2bbbd3 100644
--- a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts
+++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts
@@ -21,6 +21,7 @@ jest.mock('./assign_bundles_to_workers.ts');
jest.mock('./kibana_platform_plugins.ts');
jest.mock('./get_plugin_bundles.ts');
jest.mock('../common/theme_tags.ts');
+jest.mock('./filter_by_id.ts');
import Path from 'path';
import Os from 'os';
@@ -113,6 +114,7 @@ describe('OptimizerConfig::parseOptions()', () => {
Object {
"cache": true,
"dist": false,
+ "filters": Array [],
"includeCoreBundle": false,
"inspectWorkers": false,
"maxWorkerCount": 2,
@@ -139,6 +141,7 @@ describe('OptimizerConfig::parseOptions()', () => {
Object {
"cache": false,
"dist": false,
+ "filters": Array [],
"includeCoreBundle": false,
"inspectWorkers": false,
"maxWorkerCount": 2,
@@ -165,6 +168,7 @@ describe('OptimizerConfig::parseOptions()', () => {
Object {
"cache": true,
"dist": false,
+ "filters": Array [],
"includeCoreBundle": false,
"inspectWorkers": false,
"maxWorkerCount": 2,
@@ -193,6 +197,7 @@ describe('OptimizerConfig::parseOptions()', () => {
Object {
"cache": true,
"dist": false,
+ "filters": Array [],
"includeCoreBundle": false,
"inspectWorkers": false,
"maxWorkerCount": 2,
@@ -218,6 +223,7 @@ describe('OptimizerConfig::parseOptions()', () => {
Object {
"cache": true,
"dist": false,
+ "filters": Array [],
"includeCoreBundle": false,
"inspectWorkers": false,
"maxWorkerCount": 2,
@@ -243,6 +249,7 @@ describe('OptimizerConfig::parseOptions()', () => {
Object {
"cache": true,
"dist": false,
+ "filters": Array [],
"includeCoreBundle": false,
"inspectWorkers": false,
"maxWorkerCount": 100,
@@ -265,6 +272,7 @@ describe('OptimizerConfig::parseOptions()', () => {
Object {
"cache": false,
"dist": false,
+ "filters": Array [],
"includeCoreBundle": false,
"inspectWorkers": false,
"maxWorkerCount": 100,
@@ -287,6 +295,7 @@ describe('OptimizerConfig::parseOptions()', () => {
Object {
"cache": false,
"dist": false,
+ "filters": Array [],
"includeCoreBundle": false,
"inspectWorkers": false,
"maxWorkerCount": 100,
@@ -310,6 +319,7 @@ describe('OptimizerConfig::parseOptions()', () => {
Object {
"cache": false,
"dist": false,
+ "filters": Array [],
"includeCoreBundle": false,
"inspectWorkers": false,
"maxWorkerCount": 100,
@@ -333,6 +343,7 @@ describe('OptimizerConfig::parseOptions()', () => {
Object {
"cache": true,
"dist": false,
+ "filters": Array [],
"includeCoreBundle": false,
"inspectWorkers": false,
"maxWorkerCount": 100,
@@ -358,6 +369,7 @@ describe('OptimizerConfig::create()', () => {
const findKibanaPlatformPlugins: jest.Mock = jest.requireMock('./kibana_platform_plugins.ts')
.findKibanaPlatformPlugins;
const getPluginBundles: jest.Mock = jest.requireMock('./get_plugin_bundles.ts').getPluginBundles;
+ const filterById: jest.Mock = jest.requireMock('./filter_by_id.ts').filterById;
beforeEach(() => {
if ('mock' in OptimizerConfig.parseOptions) {
@@ -370,6 +382,7 @@ describe('OptimizerConfig::create()', () => {
]);
findKibanaPlatformPlugins.mockReturnValue(Symbol('new platform plugins'));
getPluginBundles.mockReturnValue([Symbol('bundle1'), Symbol('bundle2')]);
+ filterById.mockReturnValue(Symbol('filtered bundles'));
jest.spyOn(OptimizerConfig, 'parseOptions').mockImplementation((): any => ({
cache: Symbol('parsed cache'),
@@ -382,6 +395,7 @@ describe('OptimizerConfig::create()', () => {
themeTags: Symbol('theme tags'),
inspectWorkers: Symbol('parsed inspect workers'),
profileWebpack: Symbol('parsed profile webpack'),
+ filters: [],
}));
});
@@ -392,10 +406,7 @@ describe('OptimizerConfig::create()', () => {
expect(config).toMatchInlineSnapshot(`
OptimizerConfig {
- "bundles": Array [
- Symbol(bundle1),
- Symbol(bundle2),
- ],
+ "bundles": Symbol(filtered bundles),
"cache": Symbol(parsed cache),
"dist": Symbol(parsed dist),
"inspectWorkers": Symbol(parsed inspect workers),
@@ -431,6 +442,32 @@ describe('OptimizerConfig::create()', () => {
}
`);
+ expect(filterById.mock).toMatchInlineSnapshot(`
+ Object {
+ "calls": Array [
+ Array [
+ Array [],
+ Array [
+ Symbol(bundle1),
+ Symbol(bundle2),
+ ],
+ ],
+ ],
+ "instances": Array [
+ [Window],
+ ],
+ "invocationCallOrder": Array [
+ 23,
+ ],
+ "results": Array [
+ Object {
+ "type": "return",
+ "value": Symbol(filtered bundles),
+ },
+ ],
+ }
+ `);
+
expect(getPluginBundles.mock).toMatchInlineSnapshot(`
Object {
"calls": Array [
diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts
index 7757004139d0d..0e588ab36238b 100644
--- a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts
+++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts
@@ -31,6 +31,7 @@ import {
import { findKibanaPlatformPlugins, KibanaPlatformPlugin } from './kibana_platform_plugins';
import { getPluginBundles } from './get_plugin_bundles';
+import { filterById } from './filter_by_id';
function pickMaxWorkerCount(dist: boolean) {
// don't break if cpus() returns nothing, or an empty array
@@ -77,6 +78,18 @@ interface Options {
pluginScanDirs?: string[];
/** absolute paths that should be added to the default scan dirs */
extraPluginScanDirs?: string[];
+ /**
+ * array of comma separated patterns that will be matched against bundle ids.
+ * bundles will only be built if they match one of the specified patterns.
+ * `*` can exist anywhere in each pattern and will match anything, `!` inverts the pattern
+ *
+ * examples:
+ * --filter foo --filter bar # [foo, bar], excludes [foobar]
+ * --filter foo,bar # [foo, bar], excludes [foobar]
+ * --filter foo* # [foo, foobar], excludes [bar]
+ * --filter f*r # [foobar], excludes [foo, bar]
+ */
+ filter?: string[];
/** flag that causes the core bundle to be built along with plugins */
includeCoreBundle?: boolean;
@@ -103,6 +116,7 @@ interface ParsedOptions {
dist: boolean;
pluginPaths: string[];
pluginScanDirs: string[];
+ filters: string[];
inspectWorkers: boolean;
includeCoreBundle: boolean;
themeTags: ThemeTags;
@@ -118,6 +132,7 @@ export class OptimizerConfig {
const inspectWorkers = !!options.inspectWorkers;
const cache = options.cache !== false && !process.env.KBN_OPTIMIZER_NO_CACHE;
const includeCoreBundle = !!options.includeCoreBundle;
+ const filters = options.filter || [];
const repoRoot = options.repoRoot;
if (!Path.isAbsolute(repoRoot)) {
@@ -172,6 +187,7 @@ export class OptimizerConfig {
cache,
pluginScanDirs,
pluginPaths,
+ filters,
inspectWorkers,
includeCoreBundle,
themeTags,
@@ -198,7 +214,7 @@ export class OptimizerConfig {
];
return new OptimizerConfig(
- bundles,
+ filterById(options.filters, bundles),
options.cache,
options.watch,
options.inspectWorkers,
diff --git a/packages/kbn-optimizer/src/worker/bundle_ref_module.ts b/packages/kbn-optimizer/src/worker/bundle_ref_module.ts
index cde25564cf528..563b4ecb4bc37 100644
--- a/packages/kbn-optimizer/src/worker/bundle_ref_module.ts
+++ b/packages/kbn-optimizer/src/worker/bundle_ref_module.ts
@@ -10,6 +10,7 @@
// @ts-ignore not typed by @types/webpack
import Module from 'webpack/lib/Module';
+import { BundleRef } from '../common';
export class BundleRefModule extends Module {
public built = false;
@@ -17,12 +18,12 @@ export class BundleRefModule extends Module {
public buildInfo?: any;
public exportsArgument = '__webpack_exports__';
- constructor(public readonly exportId: string) {
+ constructor(public readonly ref: BundleRef) {
super('kbn/bundleRef', null);
}
libIdent() {
- return this.exportId;
+ return this.ref.exportId;
}
chunkCondition(chunk: any) {
@@ -30,7 +31,7 @@ export class BundleRefModule extends Module {
}
identifier() {
- return '@kbn/bundleRef ' + JSON.stringify(this.exportId);
+ return '@kbn/bundleRef ' + JSON.stringify(this.ref.exportId);
}
readableIdentifier() {
@@ -51,7 +52,7 @@ export class BundleRefModule extends Module {
source() {
return `
__webpack_require__.r(__webpack_exports__);
- var ns = __kbnBundles__.get('${this.exportId}');
+ var ns = __kbnBundles__.get('${this.ref.exportId}');
Object.defineProperties(__webpack_exports__, Object.getOwnPropertyDescriptors(ns))
`;
}
diff --git a/packages/kbn-optimizer/src/worker/bundle_refs_plugin.ts b/packages/kbn-optimizer/src/worker/bundle_refs_plugin.ts
index 9c4d5ed7f8a98..5396d11726f7a 100644
--- a/packages/kbn-optimizer/src/worker/bundle_refs_plugin.ts
+++ b/packages/kbn-optimizer/src/worker/bundle_refs_plugin.ts
@@ -44,6 +44,7 @@ export class BundleRefsPlugin {
private readonly resolvedRefEntryCache = new Map>();
private readonly resolvedRequestCache = new Map>();
private readonly ignorePrefix = Path.resolve(this.bundle.contextDir) + Path.sep;
+ private allowedBundleIds = new Set();
constructor(private readonly bundle: Bundle, private readonly bundleRefs: BundleRefs) {}
@@ -81,6 +82,45 @@ export class BundleRefsPlugin {
}
);
});
+
+ compiler.hooks.compilation.tap('BundleRefsPlugin/getRequiredBundles', (compilation) => {
+ this.allowedBundleIds.clear();
+
+ const manifestPath = this.bundle.manifestPath;
+ if (!manifestPath) {
+ return;
+ }
+
+ const deps = this.bundle.readBundleDeps();
+ for (const ref of this.bundleRefs.forBundleIds([...deps.explicit, ...deps.implicit])) {
+ this.allowedBundleIds.add(ref.bundleId);
+ }
+
+ compilation.hooks.additionalAssets.tap('BundleRefsPlugin/watchManifest', () => {
+ compilation.fileDependencies.add(manifestPath);
+ });
+
+ compilation.hooks.finishModules.tapPromise(
+ 'BundleRefsPlugin/finishModules',
+ async (modules) => {
+ const usedBundleIds = (modules as any[])
+ .filter((m: any): m is BundleRefModule => m instanceof BundleRefModule)
+ .map((m) => m.ref.bundleId);
+
+ const unusedBundleIds = deps.explicit
+ .filter((id) => !usedBundleIds.includes(id))
+ .join(', ');
+
+ if (unusedBundleIds) {
+ const error = new Error(
+ `Bundle for [${this.bundle.id}] lists [${unusedBundleIds}] as a required bundle, but does not use it. Please remove it.`
+ );
+ (error as any).file = manifestPath;
+ compilation.errors.push(error);
+ }
+ }
+ );
+ });
}
private cachedResolveRefEntry(ref: BundleRef) {
@@ -170,21 +210,29 @@ export class BundleRefsPlugin {
return;
}
- const eligibleRefs = this.bundleRefs.filterByContextPrefix(this.bundle, resolved);
- if (!eligibleRefs.length) {
+ const possibleRefs = this.bundleRefs.filterByContextPrefix(this.bundle, resolved);
+ if (!possibleRefs.length) {
// import doesn't match a bundle context
return;
}
- for (const ref of eligibleRefs) {
+ for (const ref of possibleRefs) {
const resolvedEntry = await this.cachedResolveRefEntry(ref);
- if (resolved === resolvedEntry) {
- return new BundleRefModule(ref.exportId);
+ if (resolved !== resolvedEntry) {
+ continue;
}
+
+ if (!this.allowedBundleIds.has(ref.bundleId)) {
+ throw new Error(
+ `import [${request}] references a public export of the [${ref.bundleId}] bundle, but that bundle is not in the "requiredPlugins" or "requiredBundles" list in the plugin manifest [${this.bundle.manifestPath}]`
+ );
+ }
+
+ return new BundleRefModule(ref);
}
- const bundleId = Array.from(new Set(eligibleRefs.map((r) => r.bundleId))).join(', ');
- const publicDir = eligibleRefs.map((r) => r.entry).join(', ');
+ const bundleId = Array.from(new Set(possibleRefs.map((r) => r.bundleId))).join(', ');
+ const publicDir = possibleRefs.map((r) => r.entry).join(', ');
throw new Error(
`import [${request}] references a non-public export of the [${bundleId}] bundle and must point to one of the public directories: [${publicDir}]`
);
diff --git a/packages/kbn-optimizer/src/worker/run_compilers.ts b/packages/kbn-optimizer/src/worker/run_compilers.ts
index ca7673748bde9..c7be943d65a48 100644
--- a/packages/kbn-optimizer/src/worker/run_compilers.ts
+++ b/packages/kbn-optimizer/src/worker/run_compilers.ts
@@ -50,6 +50,15 @@ import {
const PLUGIN_NAME = '@kbn/optimizer';
+/**
+ * sass-loader creates about a 40% overhead on the overall optimizer runtime, and
+ * so this constant is used to indicate to assignBundlesToWorkers() that there is
+ * extra work done in a bundle that has a lot of scss imports. The value is
+ * arbitrary and just intended to weigh the bundles so that they are distributed
+ * across mulitple workers on machines with lots of cores.
+ */
+const EXTRA_SCSS_WORK_UNITS = 100;
+
/**
* Create an Observable for a specific child compiler + bundle
*/
@@ -102,6 +111,11 @@ const observeCompiler = (
const bundleRefExportIds: string[] = [];
const referencedFiles = new Set();
let normalModuleCount = 0;
+ let workUnits = stats.compilation.fileDependencies.size;
+
+ if (bundle.manifestPath) {
+ referencedFiles.add(bundle.manifestPath);
+ }
for (const module of stats.compilation.modules) {
if (isNormalModule(module)) {
@@ -111,6 +125,15 @@ const observeCompiler = (
if (!parsedPath.dirs.includes('node_modules')) {
referencedFiles.add(path);
+
+ if (path.endsWith('.scss')) {
+ workUnits += EXTRA_SCSS_WORK_UNITS;
+
+ for (const depPath of module.buildInfo.fileDependencies) {
+ referencedFiles.add(depPath);
+ }
+ }
+
continue;
}
@@ -127,7 +150,7 @@ const observeCompiler = (
}
if (module instanceof BundleRefModule) {
- bundleRefExportIds.push(module.exportId);
+ bundleRefExportIds.push(module.ref.exportId);
continue;
}
@@ -158,6 +181,7 @@ const observeCompiler = (
optimizerCacheKey: workerConfig.optimizerCacheKey,
cacheKey: bundle.createCacheKey(files, mtimes),
moduleCount: normalModuleCount,
+ workUnits,
files,
});
diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts
index 6cbdc5ec7fc20..e1d3bf1a8d901 100644
--- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts
+++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts
@@ -148,7 +148,7 @@ export const schema = Joi.object()
browser: Joi.object()
.keys({
- type: Joi.string().valid('chrome', 'firefox', 'ie', 'msedge').default('chrome'),
+ type: Joi.string().valid('chrome', 'firefox', 'msedge').default('chrome'),
logPollingMs: Joi.number().default(100),
acceptInsecureCerts: Joi.boolean().default(false),
diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js
index 90bea1c3aa293..f4200d6f47574 100644
--- a/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js
+++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js
@@ -36,6 +36,8 @@ export function MochaReporterProvider({ getService }) {
let originalLogWriters;
let reporterCaptureStartTime;
+ const failuresOverTime = [];
+
return class MochaReporter extends Mocha.reporters.Base {
constructor(runner, options) {
super(runner, options);
@@ -154,30 +156,50 @@ export function MochaReporterProvider({ getService }) {
// - I started by trying to extract the Base.list() logic from mocha
// but it's a lot more complicated than this is horrible.
// - In order to fix the numbering and indentation we monkey-patch
- // console.log and parse the logged output.
+ // Mocha.reporters.Base.consoleLog and parse the logged output.
//
let output = '';
- const realLog = console.log;
- console.log = (...args) => (output += `${format(...args)}\n`);
+ const realLog = Mocha.reporters.Base.consoleLog;
+ Mocha.reporters.Base.consoleLog = (...args) => (output += `${format(...args)}\n`);
try {
Mocha.reporters.Base.list([runnable]);
} finally {
- console.log = realLog;
+ Mocha.reporters.Base.consoleLog = realLog;
}
+ const outputLines = output.split('\n');
+
+ const errorMarkerStart = outputLines.reduce((index, line, i) => {
+ if (index >= 0) {
+ return index;
+ }
+ return /Error:/.test(line) ? i : index;
+ }, -1);
+
+ const errorMessage = outputLines
+ // drop the first ${errorMarkerStart} lines, (empty + test title)
+ .slice(errorMarkerStart)
+ // move leading colors behind leading spaces
+ .map((line) => line.replace(/^((?:\[.+m)+)(\s+)/, '$2$1'))
+ .map((line) => ` ${line}`)
+ .join('\n');
+
log.write(
- `- ${colors.fail(`${symbols.err} fail: "${runnable.fullTitle()}"`)}` +
- '\n' +
- output
- .split('\n')
- // drop the first two lines, (empty + test title)
- .slice(2)
- // move leading colors behind leading spaces
- .map((line) => line.replace(/^((?:\[.+m)+)(\s+)/, '$2$1'))
- .map((line) => ` ${line}`)
- .join('\n')
+ `- ${colors.fail(`${symbols.err} fail: ${runnable.fullTitle()}`)}` + '\n' + errorMessage
);
+ // Prefer to reuse the nice Mocha nested title format for final summary
+ const nestedTitleFormat = outputLines
+ .slice(1, errorMarkerStart)
+ .join('\n')
+ // make sure to remove the list number
+ .replace(/\d+\)/, '');
+
+ failuresOverTime.push({
+ title: nestedTitleFormat,
+ error: errorMessage,
+ });
+
// failed hooks trigger the `onFail(runnable)` callback, so we snapshot the logs for
// them here. Tests will re-capture the snapshot in `onTestEnd()`
snapshotLogsForRunnable(runnable);
@@ -188,7 +210,7 @@ export function MochaReporterProvider({ getService }) {
log.setWriters(originalLogWriters);
}
- writeEpilogue(log, this.stats);
+ writeEpilogue(log, this.stats, failuresOverTime);
};
};
}
diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/write_epilogue.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/write_epilogue.js
index 72a011ce510bc..0ee429067254b 100644
--- a/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/write_epilogue.js
+++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/write_epilogue.js
@@ -20,7 +20,7 @@
import * as colors from './colors';
import { ms } from './ms';
-export function writeEpilogue(log, stats) {
+export function writeEpilogue(log, stats, failuresDetail) {
// header
log.write('');
@@ -35,6 +35,12 @@ export function writeEpilogue(log, stats) {
// failures
if (stats.failures) {
log.write('%d failing', stats.failures);
+ log.write('');
+ failuresDetail.forEach(({ title, error }, i) => {
+ log.write('%d) %s', i + 1, title);
+ log.write('');
+ log.write('%s', error);
+ });
}
// footer
diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json
index 6ea4a621f92f6..8398d1c081da6 100644
--- a/packages/kbn-ui-shared-deps/package.json
+++ b/packages/kbn-ui-shared-deps/package.json
@@ -10,7 +10,7 @@
},
"dependencies": {
"@elastic/charts": "19.8.1",
- "@elastic/eui": "24.1.0",
+ "@elastic/eui": "26.3.1",
"@elastic/numeral": "^2.5.0",
"@kbn/i18n": "1.0.0",
"@kbn/monaco": "1.0.0",
diff --git a/src/cli/cluster/cluster_manager.ts b/src/cli/cluster/cluster_manager.ts
index 6db6199b391e1..f193f33e6f47e 100644
--- a/src/cli/cluster/cluster_manager.ts
+++ b/src/cli/cluster/cluster_manager.ts
@@ -261,7 +261,7 @@ export class ClusterManager {
/debug\.log$/,
...pluginInternalDirsIgnore,
fromRoot('src/legacy/server/sass/__tmp__'),
- fromRoot('x-pack/plugins/reporting/.chromium'),
+ fromRoot('x-pack/plugins/reporting/chromium'),
fromRoot('x-pack/plugins/security_solution/cypress'),
fromRoot('x-pack/plugins/apm/e2e'),
fromRoot('x-pack/plugins/apm/scripts'),
diff --git a/src/core/public/application/scoped_history.mock.ts b/src/core/public/application/scoped_history.mock.ts
index 56de97e630bf0..41c72306a99f9 100644
--- a/src/core/public/application/scoped_history.mock.ts
+++ b/src/core/public/application/scoped_history.mock.ts
@@ -27,7 +27,8 @@ const createMock = ({
hash = '',
key,
state,
-}: Partial = {}) => {
+ ...overrides
+}: Partial = {}) => {
const mock: ScopedHistoryMock = {
block: jest.fn(),
createHref: jest.fn(),
@@ -38,6 +39,7 @@ const createMock = ({
listen: jest.fn(),
push: jest.fn(),
replace: jest.fn(),
+ ...overrides,
action: 'PUSH',
length: 1,
location: {
diff --git a/src/core/public/chrome/chrome_service.tsx b/src/core/public/chrome/chrome_service.tsx
index 1b894bc400f08..d29120e6ee9ac 100644
--- a/src/core/public/chrome/chrome_service.tsx
+++ b/src/core/public/chrome/chrome_service.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
-import { Breadcrumb as EuiBreadcrumb, IconType } from '@elastic/eui';
+import { EuiBreadcrumb, IconType } from '@elastic/eui';
import React from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { BehaviorSubject, combineLatest, merge, Observable, of, ReplaySubject } from 'rxjs';
diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap
index 9fee7b50f371b..9ecbc055e3320 100644
--- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap
+++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap
@@ -149,7 +149,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
"euiIconType": "logoSecurity",
"id": "security",
"label": "Security",
- "order": 3000,
+ "order": 4000,
},
"data-test-subj": "siem",
"href": "siem",
@@ -164,7 +164,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
"euiIconType": "logoObservability",
"id": "observability",
"label": "Observability",
- "order": 2000,
+ "order": 3000,
},
"data-test-subj": "metrics",
"href": "metrics",
@@ -233,7 +233,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
"euiIconType": "logoObservability",
"id": "observability",
"label": "Observability",
- "order": 2000,
+ "order": 3000,
},
"data-test-subj": "logs",
"href": "logs",
@@ -372,12 +372,13 @@ exports[`CollapsibleNav renders links grouped by category 1`] = `
handler={[Function]}
/>
}
/>
@@ -3908,16 +3909,9 @@ exports[`CollapsibleNav renders the default nav 2`] = `
handler={[Function]}
/>
-
- }
- />
-
+ />
@@ -6914,7 +6914,7 @@ exports[`Header renders 3`] = `
>
@@ -12347,7 +12347,7 @@ exports[`Header renders 4`] = `
>
diff --git a/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap
index fdaa17c279a10..5080b23e99c25 100644
--- a/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap
+++ b/src/core/public/chrome/ui/header/__snapshots__/header_breadcrumbs.test.tsx.snap
@@ -13,29 +13,14 @@ exports[`HeaderBreadcrumbs renders updates to the breadcrumbs$ observable 1`] =
exports[`HeaderBreadcrumbs renders updates to the breadcrumbs$ observable 2`] = `
Array [
-
-
- First
-
- ,
-
First
- ,
+ ,
diff --git a/src/core/public/plugins/plugin.test.ts b/src/core/public/plugins/plugin.test.ts
index 3f77161f8c34d..7331157951dec 100644
--- a/src/core/public/plugins/plugin.test.ts
+++ b/src/core/public/plugins/plugin.test.ts
@@ -32,6 +32,7 @@ function createManifest(
configPath: ['path'],
requiredPlugins: required,
optionalPlugins: optional,
+ requiredBundles: [],
} as DiscoveredPlugin;
}
diff --git a/src/core/public/plugins/plugins_service.test.ts b/src/core/public/plugins/plugins_service.test.ts
index 7dc5f3655fca0..28cbfce3fd651 100644
--- a/src/core/public/plugins/plugins_service.test.ts
+++ b/src/core/public/plugins/plugins_service.test.ts
@@ -73,6 +73,7 @@ function createManifest(
configPath: ['path'],
requiredPlugins: required,
optionalPlugins: optional,
+ requiredBundles: [],
};
}
diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md
index 86e281a49b744..303d005197588 100644
--- a/src/core/public/public.api.md
+++ b/src/core/public/public.api.md
@@ -6,7 +6,6 @@
import { Action } from 'history';
import Boom from 'boom';
-import { Breadcrumb } from '@elastic/eui';
import { BulkIndexDocumentsParams } from 'elasticsearch';
import { CatAliasesParams } from 'elasticsearch';
import { CatAllocationParams } from 'elasticsearch';
@@ -37,6 +36,7 @@ import { DeleteDocumentByQueryParams } from 'elasticsearch';
import { DeleteDocumentParams } from 'elasticsearch';
import { DeleteScriptParams } from 'elasticsearch';
import { DeleteTemplateParams } from 'elasticsearch';
+import { EuiBreadcrumb } from '@elastic/eui';
import { EuiButtonEmptyProps } from '@elastic/eui';
import { EuiConfirmModalProps } from '@elastic/eui';
import { EuiGlobalToastListToast } from '@elastic/eui';
@@ -334,7 +334,7 @@ export interface ChromeBrand {
}
// @public (undocumented)
-export type ChromeBreadcrumb = Breadcrumb;
+export type ChromeBreadcrumb = EuiBreadcrumb;
// @public
export interface ChromeDocTitle {
@@ -582,6 +582,12 @@ export const DEFAULT_APP_CATEGORIES: Readonly<{
euiIconType: string;
order: number;
};
+ enterpriseSearch: {
+ id: string;
+ label: string;
+ order: number;
+ euiIconType: string;
+ };
observability: {
id: string;
label: string;
diff --git a/src/core/public/rendering/_base.scss b/src/core/public/rendering/_base.scss
index 1f9e35e62ddcc..211e9c03beea5 100644
--- a/src/core/public/rendering/_base.scss
+++ b/src/core/public/rendering/_base.scss
@@ -1,4 +1,4 @@
-@import '@elastic/eui/src/components/header/variables';
+@import '@elastic/eui/src/global_styling/variables/header';
@import '@elastic/eui/src/components/nav_drawer/variables';
/**
diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts
index ae4cf612e92ce..f8f04c59766b3 100644
--- a/src/core/server/legacy/legacy_service.test.ts
+++ b/src/core/server/legacy/legacy_service.test.ts
@@ -109,6 +109,7 @@ beforeEach(() => {
[
'plugin-id',
{
+ requiredBundles: [],
publicTargetDir: 'path/to/target/public',
publicAssetsDir: '/plugins/name/assets/',
},
diff --git a/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts b/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts
index 5ffdef88104c8..64d1256be2f30 100644
--- a/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts
+++ b/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts
@@ -302,6 +302,7 @@ test('set defaults for all missing optional fields', async () => {
kibanaVersion: '7.0.0',
optionalPlugins: [],
requiredPlugins: [],
+ requiredBundles: [],
server: true,
ui: false,
});
@@ -331,6 +332,7 @@ test('return all set optional fields as they are in manifest', async () => {
version: 'some-version',
kibanaVersion: '7.0.0',
optionalPlugins: ['some-optional-plugin'],
+ requiredBundles: [],
requiredPlugins: ['some-required-plugin', 'some-required-plugin-2'],
server: false,
ui: true,
@@ -361,6 +363,7 @@ test('return manifest when plugin expected Kibana version matches actual version
kibanaVersion: '7.0.0-alpha2',
optionalPlugins: [],
requiredPlugins: ['some-required-plugin'],
+ requiredBundles: [],
server: true,
ui: false,
});
@@ -390,6 +393,7 @@ test('return manifest when plugin expected Kibana version is `kibana`', async ()
kibanaVersion: 'kibana',
optionalPlugins: [],
requiredPlugins: ['some-required-plugin'],
+ requiredBundles: [],
server: true,
ui: true,
});
diff --git a/src/core/server/plugins/discovery/plugin_manifest_parser.ts b/src/core/server/plugins/discovery/plugin_manifest_parser.ts
index f2c3a29eca0ac..0d33e266c37db 100644
--- a/src/core/server/plugins/discovery/plugin_manifest_parser.ts
+++ b/src/core/server/plugins/discovery/plugin_manifest_parser.ts
@@ -58,6 +58,7 @@ const KNOWN_MANIFEST_FIELDS = (() => {
ui: true,
server: true,
extraPublicDirs: true,
+ requiredBundles: true,
};
return new Set(Object.keys(manifestFields));
@@ -191,6 +192,7 @@ export async function parseManifest(
configPath: manifest.configPath || snakeCase(manifest.id),
requiredPlugins: Array.isArray(manifest.requiredPlugins) ? manifest.requiredPlugins : [],
optionalPlugins: Array.isArray(manifest.optionalPlugins) ? manifest.optionalPlugins : [],
+ requiredBundles: Array.isArray(manifest.requiredBundles) ? manifest.requiredBundles : [],
ui: includesUiPlugin,
server: includesServerPlugin,
extraPublicDirs: manifest.extraPublicDirs,
diff --git a/src/core/server/plugins/integration_tests/plugins_service.test.ts b/src/core/server/plugins/integration_tests/plugins_service.test.ts
index e676c789449ca..49c129d0ae67d 100644
--- a/src/core/server/plugins/integration_tests/plugins_service.test.ts
+++ b/src/core/server/plugins/integration_tests/plugins_service.test.ts
@@ -43,6 +43,7 @@ describe('PluginsService', () => {
disabled = false,
version = 'some-version',
requiredPlugins = [],
+ requiredBundles = [],
optionalPlugins = [],
kibanaVersion = '7.0.0',
configPath = [path],
@@ -53,6 +54,7 @@ describe('PluginsService', () => {
disabled?: boolean;
version?: string;
requiredPlugins?: string[];
+ requiredBundles?: string[];
optionalPlugins?: string[];
kibanaVersion?: string;
configPath?: ConfigPath;
@@ -68,6 +70,7 @@ describe('PluginsService', () => {
configPath: `${configPath}${disabled ? '-disabled' : ''}`,
kibanaVersion,
requiredPlugins,
+ requiredBundles,
optionalPlugins,
server,
ui,
diff --git a/src/core/server/plugins/plugin.test.ts b/src/core/server/plugins/plugin.test.ts
index ec0a3986b4877..4f26686e1f5e0 100644
--- a/src/core/server/plugins/plugin.test.ts
+++ b/src/core/server/plugins/plugin.test.ts
@@ -54,6 +54,7 @@ function createPluginManifest(manifestProps: Partial = {}): Plug
kibanaVersion: '7.0.0',
requiredPlugins: ['some-required-dep'],
optionalPlugins: ['some-optional-dep'],
+ requiredBundles: [],
server: true,
ui: true,
...manifestProps,
diff --git a/src/core/server/plugins/plugin.ts b/src/core/server/plugins/plugin.ts
index 2e5881c651843..8bd9840855654 100644
--- a/src/core/server/plugins/plugin.ts
+++ b/src/core/server/plugins/plugin.ts
@@ -53,6 +53,7 @@ export class PluginWrapper<
public readonly configPath: PluginManifest['configPath'];
public readonly requiredPlugins: PluginManifest['requiredPlugins'];
public readonly optionalPlugins: PluginManifest['optionalPlugins'];
+ public readonly requiredBundles: PluginManifest['requiredBundles'];
public readonly includesServerPlugin: PluginManifest['server'];
public readonly includesUiPlugin: PluginManifest['ui'];
@@ -81,6 +82,7 @@ export class PluginWrapper<
this.configPath = params.manifest.configPath;
this.requiredPlugins = params.manifest.requiredPlugins;
this.optionalPlugins = params.manifest.optionalPlugins;
+ this.requiredBundles = params.manifest.requiredBundles;
this.includesServerPlugin = params.manifest.server;
this.includesUiPlugin = params.manifest.ui;
}
diff --git a/src/core/server/plugins/plugin_context.test.ts b/src/core/server/plugins/plugin_context.test.ts
index 69b354661abc9..ebd068caadfb9 100644
--- a/src/core/server/plugins/plugin_context.test.ts
+++ b/src/core/server/plugins/plugin_context.test.ts
@@ -43,6 +43,7 @@ function createPluginManifest(manifestProps: Partial = {}): Plug
configPath: 'path',
kibanaVersion: '7.0.0',
requiredPlugins: ['some-required-dep'],
+ requiredBundles: [],
optionalPlugins: ['some-optional-dep'],
server: true,
ui: true,
diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts
index 46fd2b00c2304..aa77335991e2c 100644
--- a/src/core/server/plugins/plugins_service.test.ts
+++ b/src/core/server/plugins/plugins_service.test.ts
@@ -64,6 +64,7 @@ const createPlugin = (
disabled = false,
version = 'some-version',
requiredPlugins = [],
+ requiredBundles = [],
optionalPlugins = [],
kibanaVersion = '7.0.0',
configPath = [path],
@@ -74,6 +75,7 @@ const createPlugin = (
disabled?: boolean;
version?: string;
requiredPlugins?: string[];
+ requiredBundles?: string[];
optionalPlugins?: string[];
kibanaVersion?: string;
configPath?: ConfigPath;
@@ -89,6 +91,7 @@ const createPlugin = (
configPath: `${configPath}${disabled ? '-disabled' : ''}`,
kibanaVersion,
requiredPlugins,
+ requiredBundles,
optionalPlugins,
server,
ui,
@@ -460,6 +463,7 @@ describe('PluginsService', () => {
id: plugin.name,
configPath: plugin.manifest.configPath,
requiredPlugins: [],
+ requiredBundles: [],
optionalPlugins: [],
},
];
@@ -563,10 +567,12 @@ describe('PluginsService', () => {
"plugin-1" => Object {
"publicAssetsDir": /path-1/public/assets,
"publicTargetDir": /path-1/target/public,
+ "requiredBundles": Array [],
},
"plugin-2" => Object {
"publicAssetsDir": /path-2/public/assets,
"publicTargetDir": /path-2/target/public,
+ "requiredBundles": Array [],
},
}
`);
diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts
index 5d1261e697bc0..06de48a215881 100644
--- a/src/core/server/plugins/plugins_service.ts
+++ b/src/core/server/plugins/plugins_service.ts
@@ -228,6 +228,7 @@ export class PluginsService implements CoreService
uiPluginNames.includes(p)
),
+ requiredBundles: plugin.manifest.requiredBundles,
},
];
})
diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts
index 9e86ee22c607b..9695c9171a771 100644
--- a/src/core/server/plugins/types.ts
+++ b/src/core/server/plugins/types.ts
@@ -136,6 +136,18 @@ export interface PluginManifest {
*/
readonly requiredPlugins: readonly PluginName[];
+ /**
+ * List of plugin ids that this plugin's UI code imports modules from that are
+ * not in `requiredPlugins`.
+ *
+ * @remarks
+ * The plugins listed here will be loaded in the browser, even if the plugin is
+ * disabled. Required by `@kbn/optimizer` to support cross-plugin imports.
+ * "core" and plugins already listed in `requiredPlugins` do not need to be
+ * duplicated here.
+ */
+ readonly requiredBundles: readonly string[];
+
/**
* An optional list of the other plugins that if installed and enabled **may be**
* leveraged by this plugin for some additional functionality but otherwise are
@@ -191,12 +203,28 @@ export interface DiscoveredPlugin {
* not required for this plugin to work properly.
*/
readonly optionalPlugins: readonly PluginName[];
+
+ /**
+ * List of plugin ids that this plugin's UI code imports modules from that are
+ * not in `requiredPlugins`.
+ *
+ * @remarks
+ * The plugins listed here will be loaded in the browser, even if the plugin is
+ * disabled. Required by `@kbn/optimizer` to support cross-plugin imports.
+ * "core" and plugins already listed in `requiredPlugins` do not need to be
+ * duplicated here.
+ */
+ readonly requiredBundles: readonly PluginName[];
}
/**
* @internal
*/
export interface InternalPluginInfo {
+ /**
+ * Bundles that must be loaded for this plugoin
+ */
+ readonly requiredBundles: readonly string[];
/**
* Path to the target/public directory of the plugin which should be
* served
diff --git a/src/core/server/saved_objects/mappings/types.ts b/src/core/server/saved_objects/mappings/types.ts
index 7521e4a4bee86..7a7955ee745e8 100644
--- a/src/core/server/saved_objects/mappings/types.ts
+++ b/src/core/server/saved_objects/mappings/types.ts
@@ -45,7 +45,9 @@
* @public
*/
export interface SavedObjectsTypeMappingDefinition {
- /** The dynamic property of the mapping. either `false` or 'strict'. Defaults to `false` */
+ /** The dynamic property of the mapping, either `false` or `'strict'`. If
+ * unspecified `dynamic: 'strict'` will be inherited from the top-level
+ * index mappings. */
dynamic?: false | 'strict';
/** The underlying properties of the type mapping */
properties: SavedObjectsMappingProperties;
@@ -134,7 +136,6 @@ export interface SavedObjectsCoreFieldMapping {
null_value?: number | boolean | string;
index?: boolean;
doc_values?: boolean;
- enabled?: boolean;
fields?: {
[subfield: string]: {
type: string;
@@ -146,14 +147,19 @@ export interface SavedObjectsCoreFieldMapping {
/**
* See {@link SavedObjectsFieldMapping} for documentation.
*
- * Note: this type intentially doesn't include a type definition for defining
- * the `dynamic` mapping parameter. Saved Object fields should always inherit
- * the `dynamic: 'strict'` paramater. If you are unsure of the shape of your
- * data use `type: 'object', enabled: false` instead.
- *
* @public
*/
export interface SavedObjectsComplexFieldMapping {
+ /**
+ * The dynamic property of the mapping, either `false` or `'strict'`. If
+ * unspecified `dynamic: 'strict'` will be inherited from the top-level
+ * index mappings.
+ *
+ * Note: To limit the number of mapping fields Saved Object types should
+ * *never* use `dynamic: true`.
+ */
+ dynamic?: false | 'strict';
+ enabled?: boolean;
doc_values?: boolean;
type?: string;
properties: SavedObjectsMappingProperties;
diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts
index 86c79cbfb5824..f8b203bf66d6a 100644
--- a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts
+++ b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts
@@ -151,7 +151,7 @@ describe('IndexMigrator', () => {
);
});
- test('retains mappings from the previous index', async () => {
+ test('retains unknown core field mappings from the previous index', async () => {
const { callCluster } = testOpts;
testOpts.mappingProperties = { foo: { type: 'text' } };
@@ -162,7 +162,7 @@ describe('IndexMigrator', () => {
aliases: {},
mappings: {
properties: {
- author: { type: 'text' },
+ unknown_core_field: { type: 'text' },
},
},
},
@@ -187,7 +187,66 @@ describe('IndexMigrator', () => {
},
},
properties: {
- author: { type: 'text' },
+ unknown_core_field: { type: 'text' },
+ foo: { type: 'text' },
+ migrationVersion: { dynamic: 'true', type: 'object' },
+ namespace: { type: 'keyword' },
+ namespaces: { type: 'keyword' },
+ type: { type: 'keyword' },
+ updated_at: { type: 'date' },
+ references: {
+ type: 'nested',
+ properties: {
+ name: { type: 'keyword' },
+ type: { type: 'keyword' },
+ id: { type: 'keyword' },
+ },
+ },
+ },
+ },
+ settings: { number_of_shards: 1, auto_expand_replicas: '0-1' },
+ },
+ index: '.kibana_2',
+ });
+ });
+
+ test('disables complex field mappings from unknown types in the previous index', async () => {
+ const { callCluster } = testOpts;
+
+ testOpts.mappingProperties = { foo: { type: 'text' } };
+
+ withIndex(callCluster, {
+ index: {
+ '.kibana_1': {
+ aliases: {},
+ mappings: {
+ properties: {
+ unknown_complex_field: { properties: { description: { type: 'text' } } },
+ },
+ },
+ },
+ },
+ });
+
+ await new IndexMigrator(testOpts).migrate();
+
+ expect(callCluster).toHaveBeenCalledWith('indices.create', {
+ body: {
+ mappings: {
+ dynamic: 'strict',
+ _meta: {
+ migrationMappingPropertyHashes: {
+ foo: '625b32086eb1d1203564cf85062dd22e',
+ migrationVersion: '4a1746014a75ade3a714e1db5763276f',
+ namespace: '2f4316de49999235636386fe51dc06c1',
+ namespaces: '2f4316de49999235636386fe51dc06c1',
+ references: '7997cf5a56cc02bdc9c93361bde732b0',
+ type: '2f4316de49999235636386fe51dc06c1',
+ updated_at: '00da57df13e94e9d98437d13ace4bfe0',
+ },
+ },
+ properties: {
+ unknown_complex_field: { dynamic: false, properties: {} },
foo: { type: 'text' },
migrationVersion: { dynamic: 'true', type: 'object' },
namespace: { type: 'keyword' },
diff --git a/src/core/server/saved_objects/migrations/core/migration_context.test.ts b/src/core/server/saved_objects/migrations/core/migration_context.test.ts
new file mode 100644
index 0000000000000..34d8d94d5ddab
--- /dev/null
+++ b/src/core/server/saved_objects/migrations/core/migration_context.test.ts
@@ -0,0 +1,88 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { disableUnknownTypeMappingFields } from './migration_context';
+
+describe('disableUnknownTypeMappingFields', () => {
+ const sourceMappings = {
+ _meta: {
+ migrationMappingPropertyHashes: {
+ unknown_type: 'md5hash',
+ unknown_core_field: 'md5hash',
+ known_type: 'oldmd5hash',
+ },
+ },
+ properties: {
+ unknown_type: {
+ properties: {
+ unused_field: { type: 'text' },
+ },
+ },
+ unknown_core_field: { type: 'keyword' },
+ known_type: {
+ properties: {
+ field_1: { type: 'text' },
+ old_field: { type: 'boolean' },
+ },
+ },
+ },
+ };
+ const activeMappings = {
+ _meta: {
+ migrationMappingPropertyHashes: {
+ known_type: 'md5hash',
+ },
+ },
+ properties: {
+ known_type: {
+ properties: {
+ new_field: { type: 'binary' },
+ field_1: { type: 'keyword' },
+ },
+ },
+ },
+ };
+ const targetMappings = disableUnknownTypeMappingFields(activeMappings, sourceMappings);
+
+ it('disables complex field mappings from unknown types in the source mappings', () => {
+ expect(targetMappings.properties.unknown_type).toEqual({ dynamic: false, properties: {} });
+ });
+
+ it('retains unknown core field mappings from the source mappings', () => {
+ expect(targetMappings.properties.unknown_core_field).toEqual({ type: 'keyword' });
+ });
+
+ it('overrides source mappings with known types from active mappings', () => {
+ expect(targetMappings.properties.known_type).toEqual({
+ properties: {
+ new_field: { type: 'binary' },
+ field_1: { type: 'keyword' }, // was type text in source mappings
+ // old_field was present in source but ommited in active mappings
+ },
+ });
+ });
+
+ it('retains the active mappings _meta ignoring any _meta fields in the source mappings', () => {
+ expect(targetMappings._meta).toEqual({
+ migrationMappingPropertyHashes: {
+ known_type: 'md5hash',
+ },
+ });
+ });
+});
diff --git a/src/core/server/saved_objects/migrations/core/migration_context.ts b/src/core/server/saved_objects/migrations/core/migration_context.ts
index 3a6145f5d9623..adf1851a1aa75 100644
--- a/src/core/server/saved_objects/migrations/core/migration_context.ts
+++ b/src/core/server/saved_objects/migrations/core/migration_context.ts
@@ -26,7 +26,11 @@
import { Logger } from 'src/core/server/logging';
import { SavedObjectsSerializer } from '../../serialization';
-import { SavedObjectsTypeMappingDefinitions } from '../../mappings';
+import {
+ SavedObjectsTypeMappingDefinitions,
+ SavedObjectsMappingProperties,
+ IndexMapping,
+} from '../../mappings';
import { buildActiveMappings } from './build_active_mappings';
import { CallCluster } from './call_cluster';
import { VersionedTransformer } from './document_migrator';
@@ -107,20 +111,68 @@ function createSourceContext(source: FullIndexInfo, alias: string) {
function createDestContext(
source: FullIndexInfo,
alias: string,
- mappingProperties: SavedObjectsTypeMappingDefinitions
+ typeMappingDefinitions: SavedObjectsTypeMappingDefinitions
): FullIndexInfo {
- const activeMappings = buildActiveMappings(mappingProperties);
+ const targetMappings = disableUnknownTypeMappingFields(
+ buildActiveMappings(typeMappingDefinitions),
+ source.mappings
+ );
return {
aliases: {},
exists: false,
indexName: nextIndexName(source.indexName, alias),
- mappings: {
- ...activeMappings,
- properties: {
- ...source.mappings.properties,
- ...activeMappings.properties,
- },
+ mappings: targetMappings,
+ };
+}
+
+/**
+ * Merges the active mappings and the source mappings while disabling the
+ * fields of any unknown Saved Object types present in the source index's
+ * mappings.
+ *
+ * Since the Saved Objects index has `dynamic: strict` defined at the
+ * top-level, only Saved Object types for which a mapping exists can be
+ * inserted into the index. To ensure that we can continue to store Saved
+ * Object documents belonging to a disabled plugin we define a mapping for all
+ * the unknown Saved Object types that were present in the source index's
+ * mappings. To limit the field count as much as possible, these unkwnown
+ * type's mappings are set to `dynamic: false`.
+ *
+ * (Since we're using the source index mappings instead of looking at actual
+ * document types in the inedx, we potentially add more "unknown types" than
+ * what would be necessary to support migrating all the data over to the
+ * target index.)
+ *
+ * @param activeMappings The mappings compiled from all the Saved Object types
+ * known to this Kibana node.
+ * @param sourceMappings The mappings of index used as the migration source.
+ * @returns The mappings that should be applied to the target index.
+ */
+export function disableUnknownTypeMappingFields(
+ activeMappings: IndexMapping,
+ sourceMappings: IndexMapping
+): IndexMapping {
+ const targetTypes = Object.keys(activeMappings.properties);
+
+ const disabledTypesProperties = Object.keys(sourceMappings.properties)
+ .filter((sourceType) => {
+ const isObjectType = 'properties' in sourceMappings.properties[sourceType];
+ // Only Object/Nested datatypes can be excluded from the field count by
+ // using `dynamic: false`.
+ return !targetTypes.includes(sourceType) && isObjectType;
+ })
+ .reduce((disabledTypesAcc, sourceType) => {
+ disabledTypesAcc[sourceType] = { dynamic: false, properties: {} };
+ return disabledTypesAcc;
+ }, {} as SavedObjectsMappingProperties);
+
+ return {
+ ...activeMappings,
+ properties: {
+ ...sourceMappings.properties,
+ ...disabledTypesProperties,
+ ...activeMappings.properties,
},
};
}
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 107edf11bb6f4..3d3e1905577d9 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -566,6 +566,12 @@ export const DEFAULT_APP_CATEGORIES: Readonly<{
euiIconType: string;
order: number;
};
+ enterpriseSearch: {
+ id: string;
+ label: string;
+ order: number;
+ euiIconType: string;
+ };
observability: {
id: string;
label: string;
@@ -631,6 +637,7 @@ export interface DiscoveredPlugin {
readonly configPath: ConfigPath;
readonly id: PluginName;
readonly optionalPlugins: readonly PluginName[];
+ readonly requiredBundles: readonly PluginName[];
readonly requiredPlugins: readonly PluginName[];
}
@@ -1678,6 +1685,7 @@ export interface PluginManifest {
readonly id: PluginName;
readonly kibanaVersion: string;
readonly optionalPlugins: readonly PluginName[];
+ readonly requiredBundles: readonly string[];
readonly requiredPlugins: readonly PluginName[];
readonly server: boolean;
readonly ui: boolean;
@@ -2022,6 +2030,9 @@ export interface SavedObjectsClientWrapperOptions {
export interface SavedObjectsComplexFieldMapping {
// (undocumented)
doc_values?: boolean;
+ dynamic?: false | 'strict';
+ // (undocumented)
+ enabled?: boolean;
// (undocumented)
properties: SavedObjectsMappingProperties;
// (undocumented)
@@ -2033,8 +2044,6 @@ export interface SavedObjectsCoreFieldMapping {
// (undocumented)
doc_values?: boolean;
// (undocumented)
- enabled?: boolean;
- // (undocumented)
fields?: {
[subfield: string]: {
type: string;
@@ -2699,8 +2708,8 @@ export const validBodyOutput: readonly ["data", "stream"];
// src/core/server/legacy/types.ts:165:3 - (ae-forgotten-export) The symbol "LegacyNavLinkSpec" needs to be exported by the entry point index.d.ts
// src/core/server/legacy/types.ts:166:3 - (ae-forgotten-export) The symbol "LegacyAppSpec" needs to be exported by the entry point index.d.ts
// src/core/server/legacy/types.ts:167:16 - (ae-forgotten-export) The symbol "LegacyPluginSpec" needs to be exported by the entry point index.d.ts
-// src/core/server/plugins/types.ts:238:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts
-// src/core/server/plugins/types.ts:238:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts
-// src/core/server/plugins/types.ts:240:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts
+// src/core/server/plugins/types.ts:266:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts
+// src/core/server/plugins/types.ts:266:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts
+// src/core/server/plugins/types.ts:268:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts
```
diff --git a/src/core/utils/default_app_categories.ts b/src/core/utils/default_app_categories.ts
index 5708bcfeac31a..cc9bfb1db04d5 100644
--- a/src/core/utils/default_app_categories.ts
+++ b/src/core/utils/default_app_categories.ts
@@ -29,20 +29,28 @@ export const DEFAULT_APP_CATEGORIES = Object.freeze({
euiIconType: 'logoKibana',
order: 1000,
},
+ enterpriseSearch: {
+ id: 'enterpriseSearch',
+ label: i18n.translate('core.ui.enterpriseSearchNavList.label', {
+ defaultMessage: 'Enterprise Search',
+ }),
+ order: 2000,
+ euiIconType: 'logoEnterpriseSearch',
+ },
observability: {
id: 'observability',
label: i18n.translate('core.ui.observabilityNavList.label', {
defaultMessage: 'Observability',
}),
euiIconType: 'logoObservability',
- order: 2000,
+ order: 3000,
},
security: {
id: 'security',
label: i18n.translate('core.ui.securityNavList.label', {
defaultMessage: 'Security',
}),
- order: 3000,
+ order: 4000,
euiIconType: 'logoSecurity',
},
management: {
diff --git a/src/dev/build/README.md b/src/dev/build/README.md
index 3b579033fabe1..ed8750f6fee56 100644
--- a/src/dev/build/README.md
+++ b/src/dev/build/README.md
@@ -24,7 +24,7 @@ The majority of this logic is extracted from the grunt build that has existed fo
**Config**: [lib/config.js] defines the config used to execute tasks. It is mostly used to determine absolute paths to specific locations, and to get access to the Platforms.
-**Platform**: [lib/platform.js] defines the Platform objects, which define the different platforms we build for. Use `config.getTargetPlatforms()` to get the list of platforms we are targeting in this build, `config.getNodePlatforms()` to get the list of platform we will download node for, or `config.getLinux/Windows/MacPlatform()` to get a specific platform.
+**Platform**: [lib/platform.js] defines the Platform objects, which define the different platforms we build for. Use `config.getTargetPlatforms()` to get the list of platforms we are targeting in this build, `config.getNodePlatforms()` to get the list of platform we will download node for, or `config.getPlatform` to get a specific platform and architecture.
**Log**: We uses the `ToolingLog` defined in [../tooling_log/tooling_log.js]
diff --git a/src/dev/build/build_distributables.js b/src/dev/build/build_distributables.js
index 2ea71fa2c1d33..22a348b78dc0a 100644
--- a/src/dev/build/build_distributables.js
+++ b/src/dev/build/build_distributables.js
@@ -20,16 +20,16 @@
import { getConfig, createRunner } from './lib';
import {
+ BuildKibanaPlatformPluginsTask,
BuildPackagesTask,
CleanClientModulesOnDLLTask,
CleanEmptyFoldersTask,
CleanExtraBinScriptsTask,
- CleanExtraBrowsersTask,
CleanExtraFilesFromModulesTask,
- CleanPackagesTask,
- CleanTypescriptTask,
CleanNodeBuildsTask,
+ CleanPackagesTask,
CleanTask,
+ CleanTypescriptTask,
CopyBinScriptsTask,
CopySourceTask,
CreateArchivesSourcesTask,
@@ -44,20 +44,20 @@ import {
CreateRpmPackageTask,
DownloadNodeBuildsTask,
ExtractNodeBuildsTask,
+ InstallChromiumTask,
InstallDependenciesTask,
- BuildKibanaPlatformPluginsTask,
OptimizeBuildTask,
PatchNativeModulesTask,
+ PathLengthTask,
RemovePackageJsonDepsTask,
RemoveWorkspacesTask,
TranspileBabelTask,
TranspileScssTask,
UpdateLicenseFileTask,
+ UuidVerificationTask,
VerifyEnvTask,
VerifyExistingNodeBuildsTask,
- PathLengthTask,
WriteShaSumsTask,
- UuidVerificationTask,
} from './tasks';
export async function buildDistributables(options) {
@@ -134,12 +134,12 @@ export async function buildDistributables(options) {
/**
* copy generic build outputs into platform-specific build
- * directories and perform platform-specific steps
+ * directories and perform platform/architecture-specific steps
*/
await run(CreateArchivesSourcesTask);
await run(PatchNativeModulesTask);
+ await run(InstallChromiumTask);
await run(CleanExtraBinScriptsTask);
- await run(CleanExtraBrowsersTask);
await run(CleanNodeBuildsTask);
await run(PathLengthTask);
diff --git a/src/dev/build/lib/__tests__/config.js b/src/dev/build/lib/__tests__/config.js
index d2f408378da25..9544fc84dc6ff 100644
--- a/src/dev/build/lib/__tests__/config.js
+++ b/src/dev/build/lib/__tests__/config.js
@@ -72,15 +72,31 @@ describe('dev/build/lib/config', () => {
});
});
+ describe('#getPlatform()', () => {
+ it('throws error when platform does not exist', async () => {
+ const { config } = await setup();
+ const fn = () => config.getPlatform('foo', 'x64');
+
+ expect(fn).to.throwException(/Unable to find platform/);
+ });
+
+ it('throws error when architecture does not exist', async () => {
+ const { config } = await setup();
+ const fn = () => config.getPlatform('linux', 'foo');
+
+ expect(fn).to.throwException(/Unable to find platform/);
+ });
+ });
+
describe('#getTargetPlatforms()', () => {
it('returns an array of all platform objects', async () => {
const { config } = await setup();
expect(
config
.getTargetPlatforms()
- .map((p) => p.getName())
+ .map((p) => p.getNodeArch())
.sort()
- ).to.eql(['darwin', 'linux', 'windows']);
+ ).to.eql(['darwin-x64', 'linux-arm64', 'linux-x64', 'win32-x64']);
});
it('returns just this platform when targetAllPlatforms = false', async () => {
@@ -99,9 +115,9 @@ describe('dev/build/lib/config', () => {
expect(
config
.getTargetPlatforms()
- .map((p) => p.getName())
+ .map((p) => p.getNodeArch())
.sort()
- ).to.eql(['darwin', 'linux', 'windows']);
+ ).to.eql(['darwin-x64', 'linux-arm64', 'linux-x64', 'win32-x64']);
});
it('returns this platform and linux, when targetAllPlatforms = false', async () => {
@@ -111,39 +127,20 @@ describe('dev/build/lib/config', () => {
if (process.platform !== 'linux') {
expect(platforms).to.have.length(2);
expect(platforms[0]).to.be(config.getPlatformForThisOs());
- expect(platforms[1]).to.be(config.getLinuxPlatform());
+ expect(platforms[1]).to.be(config.getPlatform('linux', 'x64'));
} else {
expect(platforms).to.have.length(1);
- expect(platforms[0]).to.be(config.getLinuxPlatform());
+ expect(platforms[0]).to.be(config.getPlatform('linux', 'x64'));
}
});
});
- describe('#getLinuxPlatform()', () => {
- it('returns the linux platform', async () => {
- const { config } = await setup();
- expect(config.getLinuxPlatform().getName()).to.be('linux');
- });
- });
-
- describe('#getWindowsPlatform()', () => {
- it('returns the windows platform', async () => {
- const { config } = await setup();
- expect(config.getWindowsPlatform().getName()).to.be('windows');
- });
- });
-
- describe('#getMacPlatform()', () => {
- it('returns the mac platform', async () => {
- const { config } = await setup();
- expect(config.getMacPlatform().getName()).to.be('darwin');
- });
- });
-
describe('#getPlatformForThisOs()', () => {
it('returns the platform that matches the arch of this machine', async () => {
const { config } = await setup();
- expect(config.getPlatformForThisOs().getName()).to.be(process.platform);
+ const currentPlatform = config.getPlatformForThisOs();
+ expect(currentPlatform.getName()).to.be(process.platform);
+ expect(currentPlatform.getArchitecture()).to.be(process.arch);
});
});
diff --git a/src/dev/build/lib/__tests__/platform.js b/src/dev/build/lib/__tests__/platform.js
index 86ef1749feca9..a7bb5670ee412 100644
--- a/src/dev/build/lib/__tests__/platform.js
+++ b/src/dev/build/lib/__tests__/platform.js
@@ -30,37 +30,39 @@ describe('src/dev/build/lib/platform', () => {
describe('getNodeArch()', () => {
it('returns the node arch for the passed name', () => {
- expect(createPlatform('windows').getNodeArch()).to.be('windows-x64');
+ expect(createPlatform('win32', 'x64').getNodeArch()).to.be('win32-x64');
});
});
describe('getBuildName()', () => {
it('returns the build name for the passed name', () => {
- expect(createPlatform('windows').getBuildName()).to.be('windows-x86_64');
+ expect(createPlatform('linux', 'arm64', 'linux-aarch64').getBuildName()).to.be(
+ 'linux-aarch64'
+ );
});
});
describe('isWindows()', () => {
- it('returns true if name is windows', () => {
- expect(createPlatform('windows').isWindows()).to.be(true);
- expect(createPlatform('linux').isWindows()).to.be(false);
- expect(createPlatform('darwin').isWindows()).to.be(false);
+ it('returns true if name is win32', () => {
+ expect(createPlatform('win32', 'x64').isWindows()).to.be(true);
+ expect(createPlatform('linux', 'x64').isWindows()).to.be(false);
+ expect(createPlatform('darwin', 'x64').isWindows()).to.be(false);
});
});
describe('isLinux()', () => {
it('returns true if name is linux', () => {
- expect(createPlatform('windows').isLinux()).to.be(false);
- expect(createPlatform('linux').isLinux()).to.be(true);
- expect(createPlatform('darwin').isLinux()).to.be(false);
+ expect(createPlatform('win32', 'x64').isLinux()).to.be(false);
+ expect(createPlatform('linux', 'x64').isLinux()).to.be(true);
+ expect(createPlatform('darwin', 'x64').isLinux()).to.be(false);
});
});
describe('isMac()', () => {
it('returns true if name is darwin', () => {
- expect(createPlatform('windows').isMac()).to.be(false);
- expect(createPlatform('linux').isMac()).to.be(false);
- expect(createPlatform('darwin').isMac()).to.be(true);
+ expect(createPlatform('win32', 'x64').isMac()).to.be(false);
+ expect(createPlatform('linux', 'x64').isMac()).to.be(false);
+ expect(createPlatform('darwin', 'x64').isMac()).to.be(true);
});
});
});
diff --git a/src/dev/build/lib/config.js b/src/dev/build/lib/config.js
index cd762d9bb1f20..36621f1c2d4ac 100644
--- a/src/dev/build/lib/config.js
+++ b/src/dev/build/lib/config.js
@@ -18,7 +18,7 @@
*/
import { dirname, resolve, relative } from 'path';
-import { platform as getOsPlatform } from 'os';
+import os from 'os';
import { getVersionInfo } from './version_info';
import { createPlatform } from './platform';
@@ -29,7 +29,12 @@ export async function getConfig({ isRelease, targetAllPlatforms, versionQualifie
const repoRoot = dirname(pkgPath);
const nodeVersion = pkg.engines.node;
- const platforms = ['darwin', 'linux', 'windows'].map(createPlatform);
+ const platforms = [
+ createPlatform('linux', 'x64', 'linux-x86_64'),
+ createPlatform('linux', 'arm64', 'linux-aarch64'),
+ createPlatform('darwin', 'x64', 'darwin-x86_64'),
+ createPlatform('win32', 'x64', 'windows-x86_64'),
+ ];
const versionInfo = await getVersionInfo({
isRelease,
@@ -101,34 +106,22 @@ export async function getConfig({ isRelease, targetAllPlatforms, versionQualifie
}
if (process.platform === 'linux') {
- return [this.getLinuxPlatform()];
+ return [this.getPlatform('linux', 'x64')];
}
- return [this.getPlatformForThisOs(), this.getLinuxPlatform()];
+ return [this.getPlatformForThisOs(), this.getPlatform('linux', 'x64')];
}
- /**
- * Get the linux platform object
- * @return {Platform}
- */
- getLinuxPlatform() {
- return platforms.find((p) => p.isLinux());
- }
+ getPlatform(name, arch) {
+ const selected = platforms.find((p) => {
+ return name === p.getName() && arch === p.getArchitecture();
+ });
- /**
- * Get the windows platform object
- * @return {Platform}
- */
- getWindowsPlatform() {
- return platforms.find((p) => p.isWindows());
- }
+ if (!selected) {
+ throw new Error(`Unable to find platform (${name}) with architecture (${arch})`);
+ }
- /**
- * Get the mac platform object
- * @return {Platform}
- */
- getMacPlatform() {
- return platforms.find((p) => p.isMac());
+ return selected;
}
/**
@@ -136,16 +129,7 @@ export async function getConfig({ isRelease, targetAllPlatforms, versionQualifie
* @return {Platform}
*/
getPlatformForThisOs() {
- switch (getOsPlatform()) {
- case 'darwin':
- return this.getMacPlatform();
- case 'win32':
- return this.getWindowsPlatform();
- case 'linux':
- return this.getLinuxPlatform();
- default:
- throw new Error(`Unable to find platform for this os`);
- }
+ return this.getPlatform(os.platform(), os.arch());
}
/**
diff --git a/src/dev/build/lib/platform.js b/src/dev/build/lib/platform.js
index ac2faa7cbdf85..ab2672615e1c5 100644
--- a/src/dev/build/lib/platform.js
+++ b/src/dev/build/lib/platform.js
@@ -17,22 +17,26 @@
* under the License.
*/
-export function createPlatform(name) {
+export function createPlatform(name, architecture, buildName) {
return new (class Platform {
getName() {
return name;
}
- getNodeArch() {
- return `${name}-x64`;
+ getArchitecture() {
+ return architecture;
}
getBuildName() {
- return `${name}-x86_64`;
+ return buildName;
+ }
+
+ getNodeArch() {
+ return `${name}-${architecture}`;
}
isWindows() {
- return name === 'windows';
+ return name === 'win32';
}
isMac() {
diff --git a/src/dev/build/tasks/clean_tasks.js b/src/dev/build/tasks/clean_tasks.js
index 31731e392e5cb..ff5c3b3a73dd3 100644
--- a/src/dev/build/tasks/clean_tasks.js
+++ b/src/dev/build/tasks/clean_tasks.js
@@ -201,45 +201,6 @@ export const CleanExtraBinScriptsTask = {
},
};
-export const CleanExtraBrowsersTask = {
- description: 'Cleaning extra browsers from platform-specific builds',
-
- async run(config, log, build) {
- const getBrowserPathsForPlatform = (platform) => {
- const reportingDir = 'x-pack/plugins/reporting';
- const chromiumDir = '.chromium';
- const chromiumPath = (p) =>
- build.resolvePathForPlatform(platform, reportingDir, chromiumDir, p);
- return (platforms) => {
- const paths = [];
- if (platforms.windows) {
- paths.push(chromiumPath('chromium-*-win32.zip'));
- paths.push(chromiumPath('chromium-*-windows.zip'));
- }
-
- if (platforms.darwin) {
- paths.push(chromiumPath('chromium-*-darwin.zip'));
- }
-
- if (platforms.linux) {
- paths.push(chromiumPath('chromium-*-linux.zip'));
- }
- return paths;
- };
- };
- for (const platform of config.getNodePlatforms()) {
- const getBrowserPaths = getBrowserPathsForPlatform(platform);
- if (platform.isWindows()) {
- await deleteAll(getBrowserPaths({ linux: true, darwin: true }), log);
- } else if (platform.isMac()) {
- await deleteAll(getBrowserPaths({ linux: true, windows: true }), log);
- } else if (platform.isLinux()) {
- await deleteAll(getBrowserPaths({ windows: true, darwin: true }), log);
- }
- }
- },
-};
-
export const CleanEmptyFoldersTask = {
description: 'Cleaning all empty folders recursively',
diff --git a/src/dev/build/tasks/create_archives_sources_task.js b/src/dev/build/tasks/create_archives_sources_task.js
index 53cf750f484a1..76f08bd3d2e4f 100644
--- a/src/dev/build/tasks/create_archives_sources_task.js
+++ b/src/dev/build/tasks/create_archives_sources_task.js
@@ -33,7 +33,7 @@ export const CreateArchivesSourcesTask = {
log.debug(
'Generic build source copied into',
- platform.getName(),
+ platform.getNodeArch(),
'specific build directory'
);
@@ -43,7 +43,7 @@ export const CreateArchivesSourcesTask = {
destination: build.resolvePathForPlatform(platform, 'node'),
});
- log.debug('Node.js copied into', platform.getName(), 'specific build directory');
+ log.debug('Node.js copied into', platform.getNodeArch(), 'specific build directory');
})
);
},
diff --git a/src/dev/build/tasks/index.js b/src/dev/build/tasks/index.js
index be675b4aa6ca4..d96e745c10776 100644
--- a/src/dev/build/tasks/index.js
+++ b/src/dev/build/tasks/index.js
@@ -18,6 +18,7 @@
*/
export * from './bin';
+export * from './build_kibana_platform_plugins';
export * from './build_packages_task';
export * from './clean_tasks';
export * from './copy_source_task';
@@ -26,18 +27,18 @@ export * from './create_archives_task';
export * from './create_empty_dirs_and_files_task';
export * from './create_package_json_task';
export * from './create_readme_task';
+export * from './install_chromium';
export * from './install_dependencies_task';
export * from './license_file_task';
-export * from './nodejs';
export * from './nodejs_modules';
+export * from './nodejs';
export * from './notice_file_task';
export * from './optimize_task';
export * from './os_packages';
export * from './patch_native_modules_task';
+export * from './path_length_task';
export * from './transpile_babel_task';
export * from './transpile_scss_task';
+export * from './uuid_verification_task';
export * from './verify_env_task';
export * from './write_sha_sums_task';
-export * from './path_length_task';
-export * from './build_kibana_platform_plugins';
-export * from './uuid_verification_task';
diff --git a/src/dev/build/tasks/install_chromium.js b/src/dev/build/tasks/install_chromium.js
new file mode 100644
index 0000000000000..c5878b23d43ae
--- /dev/null
+++ b/src/dev/build/tasks/install_chromium.js
@@ -0,0 +1,44 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// eslint-disable-next-line @kbn/eslint/no-restricted-paths
+import { installBrowser } from '../../../../x-pack/plugins/reporting/server/browsers/install';
+import { first } from 'rxjs/operators';
+
+export const InstallChromiumTask = {
+ description: 'Installing Chromium',
+
+ async run(config, log, build) {
+ if (build.isOss()) {
+ return;
+ } else {
+ for (const platform of config.getNodePlatforms()) {
+ log.info(`Installing Chromium for ${platform.getName()}-${platform.getArchitecture()}`);
+
+ const { binaryPath$ } = installBrowser(
+ log,
+ build.resolvePathForPlatform(platform, 'x-pack/plugins/reporting/chromium'),
+ platform.getName(),
+ platform.getArchitecture()
+ );
+ await binaryPath$.pipe(first()).toPromise();
+ }
+ }
+ },
+};
diff --git a/src/dev/build/tasks/notice_file_task.js b/src/dev/build/tasks/notice_file_task.js
index 36a92f59314e2..59369c7cb5a3b 100644
--- a/src/dev/build/tasks/notice_file_task.js
+++ b/src/dev/build/tasks/notice_file_task.js
@@ -47,7 +47,7 @@ export const CreateNoticeFileTask = {
log.info('Generating build notice');
const { extractDir: nodeDir, version: nodeVersion } = getNodeDownloadInfo(
config,
- config.getLinuxPlatform()
+ config.getPlatform('linux', 'x64')
);
const notice = await generateBuildNoticeText({
diff --git a/src/dev/build/tasks/os_packages/run_fpm.js b/src/dev/build/tasks/os_packages/run_fpm.js
index 0496bcf08fb91..eb77da0e70176 100644
--- a/src/dev/build/tasks/os_packages/run_fpm.js
+++ b/src/dev/build/tasks/os_packages/run_fpm.js
@@ -22,7 +22,7 @@ import { resolve } from 'path';
import { exec } from '../../lib';
export async function runFpm(config, log, build, type, pkgSpecificFlags) {
- const linux = config.getLinuxPlatform();
+ const linux = config.getPlatform('linux', 'x64');
const version = config.getBuildVersion();
const resolveWithTrailingSlash = (...paths) => `${resolve(...paths)}/`;
diff --git a/src/dev/build/tasks/patch_native_modules_task.js b/src/dev/build/tasks/patch_native_modules_task.js
index fba33442fad10..a10010ed5255f 100644
--- a/src/dev/build/tasks/patch_native_modules_task.js
+++ b/src/dev/build/tasks/patch_native_modules_task.js
@@ -38,7 +38,7 @@ const packages = [
url: 'https://github.com/uhop/node-re2/releases/download/1.14.0/linux-x64-64.gz',
sha256: 'f54f059035e71a7ccb3fa201080e260c41d228d13a8247974b4bb157691b6757',
},
- windows: {
+ win32: {
url: 'https://github.com/uhop/node-re2/releases/download/1.14.0/win32-x64-64.gz',
sha256: 'de708446a8b802f4634c2cfef097c2625a2811fdcd8133dfd7b7c485f966caa9',
},
diff --git a/src/dev/ci_setup/checkout_sibling_es.sh b/src/dev/ci_setup/checkout_sibling_es.sh
index 3832ec9b4076a..915759d4214f9 100755
--- a/src/dev/ci_setup/checkout_sibling_es.sh
+++ b/src/dev/ci_setup/checkout_sibling_es.sh
@@ -7,11 +7,10 @@ function checkout_sibling {
targetDir=$2
useExistingParamName=$3
useExisting="$(eval "echo "\$$useExistingParamName"")"
- repoAddress="https://github.com/"
if [ -z ${useExisting:+x} ]; then
if [ -d "$targetDir" ]; then
- echo "I expected a clean workspace but an '${project}' sibling directory already exists in [$WORKSPACE]!"
+ echo "I expected a clean workspace but an '${project}' sibling directory already exists in [$PARENT_DIR]!"
echo
echo "Either define '${useExistingParamName}' or remove the existing '${project}' sibling."
exit 1
@@ -22,9 +21,8 @@ function checkout_sibling {
cloneBranch=""
function clone_target_is_valid {
-
echo " -> checking for '${cloneBranch}' branch at ${cloneAuthor}/${project}"
- if [[ -n "$(git ls-remote --heads "${repoAddress}${cloneAuthor}/${project}.git" ${cloneBranch} 2>/dev/null)" ]]; then
+ if [[ -n "$(git ls-remote --heads "git@github.com:${cloneAuthor}/${project}.git" ${cloneBranch} 2>/dev/null)" ]]; then
return 0
else
return 1
@@ -73,7 +71,7 @@ function checkout_sibling {
fi
echo " -> checking out '${cloneBranch}' branch from ${cloneAuthor}/${project}..."
- git clone -b "$cloneBranch" "${repoAddress}${cloneAuthor}/${project}.git" "$targetDir" --depth=1
+ git clone -b "$cloneBranch" "git@github.com:${cloneAuthor}/${project}.git" "$targetDir" --depth=1
echo " -> checked out ${project} revision: $(git -C "${targetDir}" rev-parse HEAD)"
echo
}
@@ -89,12 +87,12 @@ function checkout_sibling {
fi
}
-checkout_sibling "elasticsearch" "${WORKSPACE}/elasticsearch" "USE_EXISTING_ES"
+checkout_sibling "elasticsearch" "${PARENT_DIR}/elasticsearch" "USE_EXISTING_ES"
export TEST_ES_FROM=${TEST_ES_FROM:-snapshot}
# Set the JAVA_HOME based on the Java property file in the ES repo
# This assumes the naming convention used on CI (ex: ~/.java/java10)
-ES_DIR="$WORKSPACE/elasticsearch"
+ES_DIR="$PARENT_DIR/elasticsearch"
ES_JAVA_PROP_PATH=$ES_DIR/.ci/java-versions.properties
diff --git a/src/dev/ci_setup/setup_env.sh b/src/dev/ci_setup/setup_env.sh
index f96a2240917e2..343ff47199375 100644
--- a/src/dev/ci_setup/setup_env.sh
+++ b/src/dev/ci_setup/setup_env.sh
@@ -53,8 +53,6 @@ export PARENT_DIR="$parentDir"
kbnBranch="$(jq -r .branch "$KIBANA_DIR/package.json")"
export KIBANA_PKG_BRANCH="$kbnBranch"
-export WORKSPACE="${WORKSPACE:-$PARENT_DIR}"
-
###
### download node
###
@@ -163,7 +161,7 @@ export -f checks-reporter-with-killswitch
source "$KIBANA_DIR/src/dev/ci_setup/load_env_keys.sh"
-ES_DIR="$WORKSPACE/elasticsearch"
+ES_DIR="$PARENT_DIR/elasticsearch"
ES_JAVA_PROP_PATH=$ES_DIR/.ci/java-versions.properties
if [[ -d "$ES_DIR" && -f "$ES_JAVA_PROP_PATH" ]]; then
diff --git a/src/dev/i18n/integrate_locale_files.test.ts b/src/dev/i18n/integrate_locale_files.test.ts
index 3bd3dc61c044f..7ff1d87f1bc55 100644
--- a/src/dev/i18n/integrate_locale_files.test.ts
+++ b/src/dev/i18n/integrate_locale_files.test.ts
@@ -21,7 +21,7 @@ import { mockMakeDirAsync, mockWriteFileAsync } from './integrate_locale_files.t
import path from 'path';
import { integrateLocaleFiles, verifyMessages } from './integrate_locale_files';
-// @ts-expect-error
+// @ts-ignore
import { normalizePath } from './utils';
const localePath = path.resolve(__dirname, '__fixtures__', 'integrate_locale_files', 'fr.json');
@@ -36,7 +36,6 @@ const defaultIntegrateOptions = {
sourceFileName: localePath,
dryRun: false,
ignoreIncompatible: false,
- ignoreMalformed: false,
ignoreMissing: false,
ignoreUnused: false,
config: {
diff --git a/src/dev/i18n/integrate_locale_files.ts b/src/dev/i18n/integrate_locale_files.ts
index f9cd6dd1971c7..d8ccccca15559 100644
--- a/src/dev/i18n/integrate_locale_files.ts
+++ b/src/dev/i18n/integrate_locale_files.ts
@@ -31,8 +31,7 @@ import {
normalizePath,
readFileAsync,
writeFileAsync,
- verifyICUMessage,
- // @ts-expect-error
+ // @ts-ignore
} from './utils';
import { I18nConfig } from './config';
@@ -42,7 +41,6 @@ export interface IntegrateOptions {
sourceFileName: string;
targetFileName?: string;
dryRun: boolean;
- ignoreMalformed: boolean;
ignoreIncompatible: boolean;
ignoreUnused: boolean;
ignoreMissing: boolean;
@@ -107,23 +105,6 @@ export function verifyMessages(
}
}
- for (const messageId of localizedMessagesIds) {
- const defaultMessage = defaultMessagesMap.get(messageId);
- if (defaultMessage) {
- try {
- const message = localizedMessagesMap.get(messageId)!;
- verifyICUMessage(message);
- } catch (err) {
- if (options.ignoreMalformed) {
- localizedMessagesMap.delete(messageId);
- options.log.warning(`Malformed translation ignored (${messageId}): ${err}`);
- } else {
- errorMessage += `\nMalformed translation (${messageId}): ${err}\n`;
- }
- }
- }
- }
-
if (errorMessage) {
throw createFailError(errorMessage);
}
diff --git a/src/dev/i18n/tasks/check_compatibility.ts b/src/dev/i18n/tasks/check_compatibility.ts
index afaf3cd875a8a..5900bf5aff252 100644
--- a/src/dev/i18n/tasks/check_compatibility.ts
+++ b/src/dev/i18n/tasks/check_compatibility.ts
@@ -22,14 +22,13 @@ import { integrateLocaleFiles, I18nConfig } from '..';
export interface I18nFlags {
fix: boolean;
- ignoreMalformed: boolean;
ignoreIncompatible: boolean;
ignoreUnused: boolean;
ignoreMissing: boolean;
}
export function checkCompatibility(config: I18nConfig, flags: I18nFlags, log: ToolingLog) {
- const { fix, ignoreIncompatible, ignoreUnused, ignoreMalformed, ignoreMissing } = flags;
+ const { fix, ignoreIncompatible, ignoreUnused, ignoreMissing } = flags;
return config.translations.map((translationsPath) => ({
task: async ({ messages }: { messages: Map }) => {
// If `fix` is set we should try apply all possible fixes and override translations file.
@@ -38,7 +37,6 @@ export function checkCompatibility(config: I18nConfig, flags: I18nFlags, log: To
ignoreIncompatible: fix || ignoreIncompatible,
ignoreUnused: fix || ignoreUnused,
ignoreMissing: fix || ignoreMissing,
- ignoreMalformed: fix || ignoreMalformed,
sourceFileName: translationsPath,
targetFileName: fix ? translationsPath : undefined,
config,
diff --git a/src/dev/i18n/utils.js b/src/dev/i18n/utils.js
index 11a002fdbf4a8..1d1c3118e0852 100644
--- a/src/dev/i18n/utils.js
+++ b/src/dev/i18n/utils.js
@@ -208,28 +208,6 @@ export function checkValuesProperty(prefixedValuesKeys, defaultMessage, messageI
}
}
-/**
- * Verifies valid ICU message.
- * @param message ICU message.
- * @param messageId ICU message id
- * @returns {undefined}
- */
-export function verifyICUMessage(message) {
- try {
- parser.parse(message);
- } catch (error) {
- if (error.name === 'SyntaxError') {
- const errorWithContext = createParserErrorMessage(message, {
- loc: {
- line: error.location.start.line,
- column: error.location.start.column - 1,
- },
- message: error.message,
- });
- throw errorWithContext;
- }
- }
-}
/**
* Extracts value references from the ICU message.
* @param message ICU message.
diff --git a/src/dev/notice/generate_notice_from_source.ts b/src/dev/notice/generate_notice_from_source.ts
index a2b05c6dc8a4e..fb74bed0f26f4 100644
--- a/src/dev/notice/generate_notice_from_source.ts
+++ b/src/dev/notice/generate_notice_from_source.ts
@@ -49,10 +49,8 @@ export async function generateNoticeFromSource({ productName, directory, log }:
ignore: [
'{node_modules,build,target,dist,data,built_assets}/**',
'packages/*/{node_modules,build,target,dist}/**',
- 'src/plugins/*/{node_modules,build,target,dist}/**',
'x-pack/{node_modules,build,target,dist,data}/**',
'x-pack/packages/*/{node_modules,build,target,dist}/**',
- 'x-pack/plugins/*/{node_modules,build,target,dist}/**',
],
};
diff --git a/src/dev/run_i18n_check.ts b/src/dev/run_i18n_check.ts
index 70eeedac2b8b6..97ea988b1de3a 100644
--- a/src/dev/run_i18n_check.ts
+++ b/src/dev/run_i18n_check.ts
@@ -36,7 +36,6 @@ run(
async ({
flags: {
'ignore-incompatible': ignoreIncompatible,
- 'ignore-malformed': ignoreMalformed,
'ignore-missing': ignoreMissing,
'ignore-unused': ignoreUnused,
'include-config': includeConfig,
@@ -49,13 +48,12 @@ run(
fix &&
(ignoreIncompatible !== undefined ||
ignoreUnused !== undefined ||
- ignoreMalformed !== undefined ||
ignoreMissing !== undefined)
) {
throw createFailError(
`${chalk.white.bgRed(
' I18N ERROR '
- )} none of the --ignore-incompatible, --ignore-malformed, --ignore-unused or --ignore-missing is allowed when --fix is set.`
+ )} none of the --ignore-incompatible, --ignore-unused or --ignore-missing is allowed when --fix is set.`
);
}
@@ -101,7 +99,6 @@ run(
checkCompatibility(
config,
{
- ignoreMalformed: !!ignoreMalformed,
ignoreIncompatible: !!ignoreIncompatible,
ignoreUnused: !!ignoreUnused,
ignoreMissing: !!ignoreMissing,
diff --git a/src/dev/run_i18n_integrate.ts b/src/dev/run_i18n_integrate.ts
index 25c3ea32783aa..23d66fae9f26e 100644
--- a/src/dev/run_i18n_integrate.ts
+++ b/src/dev/run_i18n_integrate.ts
@@ -31,7 +31,6 @@ run(
'ignore-incompatible': ignoreIncompatible = false,
'ignore-missing': ignoreMissing = false,
'ignore-unused': ignoreUnused = false,
- 'ignore-malformed': ignoreMalformed = false,
'include-config': includeConfig,
path,
source,
@@ -67,13 +66,12 @@ run(
typeof ignoreIncompatible !== 'boolean' ||
typeof ignoreUnused !== 'boolean' ||
typeof ignoreMissing !== 'boolean' ||
- typeof ignoreMalformed !== 'boolean' ||
typeof dryRun !== 'boolean'
) {
throw createFailError(
`${chalk.white.bgRed(
' I18N ERROR '
- )} --ignore-incompatible, --ignore-unused, --ignore-malformed, --ignore-missing, and --dry-run can't have values`
+ )} --ignore-incompatible, --ignore-unused, --ignore-missing, and --dry-run can't have values`
);
}
@@ -99,7 +97,6 @@ run(
ignoreIncompatible,
ignoreUnused,
ignoreMissing,
- ignoreMalformed,
config,
log,
});
diff --git a/src/dev/storybook/aliases.ts b/src/dev/storybook/aliases.ts
index 85bfd4a7a4d26..9d9f5616b5a33 100644
--- a/src/dev/storybook/aliases.ts
+++ b/src/dev/storybook/aliases.ts
@@ -26,4 +26,5 @@ export const storybookAliases = {
infra: 'x-pack/legacy/plugins/infra/scripts/storybook.js',
security_solution: 'x-pack/plugins/security_solution/scripts/storybook.js',
ui_actions_enhanced: 'x-pack/plugins/ui_actions_enhanced/scripts/storybook.js',
+ observability: 'x-pack/plugins/observability/scripts/storybook.js',
};
diff --git a/src/legacy/core_plugins/timelion/public/_app.scss b/src/legacy/core_plugins/timelion/public/_app.scss
index e44321f26e8dd..3142e1d23cf10 100644
--- a/src/legacy/core_plugins/timelion/public/_app.scss
+++ b/src/legacy/core_plugins/timelion/public/_app.scss
@@ -1,4 +1,4 @@
-@import '@elastic/eui/src/components/header/variables';
+@import '@elastic/eui/src/global_styling/variables/header';
.timApp {
position: relative;
diff --git a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js b/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js
index 2102b02194bc8..8b4c28a50b732 100644
--- a/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js
+++ b/src/legacy/core_plugins/timelion/public/directives/timelion_expression_input.js
@@ -51,7 +51,7 @@ import {
suggest,
insertAtLocation,
} from './timelion_expression_input_helpers';
-import { comboBoxKeyCodes } from '@elastic/eui';
+import { comboBoxKeys } from '@elastic/eui';
import { npStart } from 'ui/new_platform';
const Parser = PEG.generate(grammar);
@@ -178,9 +178,9 @@ export function TimelionExpInput($http, $timeout) {
});
}
- function isNavigationalKey(keyCode) {
- const keyCodes = _.values(comboBoxKeyCodes);
- return keyCodes.includes(keyCode);
+ function isNavigationalKey(key) {
+ const keyCodes = _.values(comboBoxKeys);
+ return keyCodes.includes(key);
}
scope.onFocusInput = () => {
@@ -196,12 +196,12 @@ export function TimelionExpInput($http, $timeout) {
scope.onKeyDownInput = (e) => {
// If we've pressed any non-navigational keys, then the user has typed something and we
// can exit early without doing any navigation. The keyup handler will pull up suggestions.
- if (!isNavigationalKey(e.keyCode)) {
+ if (!isNavigationalKey(e.key)) {
return;
}
switch (e.keyCode) {
- case comboBoxKeyCodes.UP:
+ case comboBoxKeys.ARROW_UP:
if (scope.suggestions.isVisible) {
// Up and down keys navigate through suggestions.
e.preventDefault();
@@ -210,7 +210,7 @@ export function TimelionExpInput($http, $timeout) {
}
break;
- case comboBoxKeyCodes.DOWN:
+ case comboBoxKeys.ARROW_DOWN:
if (scope.suggestions.isVisible) {
// Up and down keys navigate through suggestions.
e.preventDefault();
@@ -219,7 +219,7 @@ export function TimelionExpInput($http, $timeout) {
}
break;
- case comboBoxKeyCodes.TAB:
+ case comboBoxKeys.TAB:
// If there are no suggestions or none is selected, the user tabs to the next input.
if (scope.suggestions.isEmpty() || scope.suggestions.index < 0) {
// Before letting the tab be handled to focus the next element
@@ -234,7 +234,7 @@ export function TimelionExpInput($http, $timeout) {
insertSuggestionIntoExpression(scope.suggestions.index);
break;
- case comboBoxKeyCodes.ENTER:
+ case comboBoxKeys.ENTER:
if (e.metaKey || e.ctrlKey) {
// Re-render the chart when the user hits CMD+ENTER.
e.preventDefault();
@@ -246,7 +246,7 @@ export function TimelionExpInput($http, $timeout) {
}
break;
- case comboBoxKeyCodes.ESCAPE:
+ case comboBoxKeys.ESCAPE:
e.preventDefault();
scope.suggestions.hide();
break;
@@ -255,7 +255,7 @@ export function TimelionExpInput($http, $timeout) {
scope.onKeyUpInput = (e) => {
// If the user isn't navigating, then we should update the suggestions based on their input.
- if (!isNavigationalKey(e.keyCode)) {
+ if (!isNavigationalKey(e.key)) {
getSuggestions();
}
};
diff --git a/src/legacy/ui/public/accessibility/__tests__/kbn_accessible_click.js b/src/legacy/ui/public/accessibility/__tests__/kbn_accessible_click.js
index 5466e7d43f566..f3b7ab29d8a14 100644
--- a/src/legacy/ui/public/accessibility/__tests__/kbn_accessible_click.js
+++ b/src/legacy/ui/public/accessibility/__tests__/kbn_accessible_click.js
@@ -22,7 +22,7 @@ import sinon from 'sinon';
import expect from '@kbn/expect';
import ngMock from 'ng_mock';
import '../kbn_accessible_click';
-import { keyCodes } from '@elastic/eui';
+import { keys } from '@elastic/eui';
describe('kbnAccessibleClick directive', () => {
let $compile;
@@ -112,14 +112,14 @@ describe('kbnAccessibleClick directive', () => {
it(`on ENTER keyup`, () => {
const e = angular.element.Event('keyup'); // eslint-disable-line new-cap
- e.keyCode = keyCodes.ENTER;
+ e.key = keys.ENTER;
element.trigger(e);
sinon.assert.calledOnce(scope.handleClick);
});
it(`on SPACE keyup`, () => {
const e = angular.element.Event('keyup'); // eslint-disable-line new-cap
- e.keyCode = keyCodes.SPACE;
+ e.key = keys.SPACE;
element.trigger(e);
sinon.assert.calledOnce(scope.handleClick);
});
diff --git a/src/legacy/ui/public/accessibility/__tests__/kbn_ui_ace_keyboard_mode.js b/src/legacy/ui/public/accessibility/__tests__/kbn_ui_ace_keyboard_mode.js
index a8a6f0cd0db2f..ce1bf95bf0fb7 100644
--- a/src/legacy/ui/public/accessibility/__tests__/kbn_ui_ace_keyboard_mode.js
+++ b/src/legacy/ui/public/accessibility/__tests__/kbn_ui_ace_keyboard_mode.js
@@ -22,7 +22,7 @@ import sinon from 'sinon';
import expect from '@kbn/expect';
import ngMock from 'ng_mock';
import '../kbn_ui_ace_keyboard_mode';
-import { keyCodes } from '@elastic/eui';
+import { keys } from '@elastic/eui';
describe('kbnUiAceKeyboardMode directive', () => {
let element;
@@ -48,7 +48,7 @@ describe('kbnUiAceKeyboardMode directive', () => {
const textarea = element.find('textarea');
sinon.spy(textarea[0], 'focus');
const ev = angular.element.Event('keydown'); // eslint-disable-line new-cap
- ev.keyCode = keyCodes.ENTER;
+ ev.key = keys.ENTER;
element.find('.kbnUiAceKeyboardHint').trigger(ev);
expect(textarea[0].focus.called).to.be(true);
expect(
@@ -61,7 +61,7 @@ describe('kbnUiAceKeyboardMode directive', () => {
const hint = element.find('.kbnUiAceKeyboardHint');
sinon.spy(hint[0], 'focus');
const ev = angular.element.Event('keydown'); // eslint-disable-line new-cap
- ev.keyCode = keyCodes.ESCAPE;
+ ev.key = keys.ESCAPE;
textarea.trigger(ev);
expect(hint[0].focus.called).to.be(true);
expect(hint.hasClass('kbnUiAceKeyboardHint-isInactive')).to.be(false);
@@ -101,7 +101,7 @@ describe('kbnUiAceKeyboardModeService', () => {
const textarea = element.find('textarea');
sinon.spy(textarea[0], 'focus');
const ev = angular.element.Event('keydown'); // eslint-disable-line new-cap
- ev.keyCode = keyCodes.ENTER;
+ ev.key = keys.ENTER;
element.find('.kbnUiAceKeyboardHint').trigger(ev);
expect(textarea[0].focus.called).to.be(true);
expect(
@@ -114,7 +114,7 @@ describe('kbnUiAceKeyboardModeService', () => {
const hint = element.find('.kbnUiAceKeyboardHint');
sinon.spy(hint[0], 'focus');
const ev = angular.element.Event('keydown'); // eslint-disable-line new-cap
- ev.keyCode = keyCodes.ESCAPE;
+ ev.key = keys.ESCAPE;
textarea.trigger(ev);
expect(hint[0].focus.called).to.be(true);
expect(hint.hasClass('kbnUiAceKeyboardHint-isInactive')).to.be(false);
diff --git a/src/legacy/ui/public/accessibility/kbn_ui_ace_keyboard_mode.js b/src/legacy/ui/public/accessibility/kbn_ui_ace_keyboard_mode.js
index 9ffcbc426e49c..88b08beb5b3d0 100644
--- a/src/legacy/ui/public/accessibility/kbn_ui_ace_keyboard_mode.js
+++ b/src/legacy/ui/public/accessibility/kbn_ui_ace_keyboard_mode.js
@@ -33,7 +33,7 @@
import angular from 'angular';
import { uiModules } from '../modules';
-import { keyCodes } from '@elastic/eui';
+import { keys } from '@elastic/eui';
let aceKeyboardModeId = 0;
@@ -72,7 +72,7 @@ uiModules
}
hint.keydown((ev) => {
- if (ev.keyCode === keyCodes.ENTER) {
+ if (ev.key === keys.ENTER) {
ev.preventDefault();
startEditing();
}
@@ -103,7 +103,7 @@ uiModules
);
uiAceTextbox.keydown((ev) => {
- if (ev.keyCode === keyCodes.ESCAPE) {
+ if (ev.key === keys.ESCAPE) {
// If the autocompletion context menu is open then we want to let ESC close it but
// **not** exit out of editing mode.
if (!isAutoCompleterOpen) {
diff --git a/src/legacy/ui/public/exit_full_screen/exit_full_screen_button.test.js b/src/legacy/ui/public/exit_full_screen/exit_full_screen_button.test.js
index 0a12988fa5d90..d4273c0fdb207 100644
--- a/src/legacy/ui/public/exit_full_screen/exit_full_screen_button.test.js
+++ b/src/legacy/ui/public/exit_full_screen/exit_full_screen_button.test.js
@@ -33,7 +33,7 @@ import chrome from 'ui/chrome';
import { ExitFullScreenButton } from './exit_full_screen_button';
-import { keyCodes } from '@elastic/eui';
+import { keys } from '@elastic/eui';
test('is rendered', () => {
const component = renderWithIntl( {}} />);
@@ -57,7 +57,7 @@ describe('onExitFullScreenMode', () => {
mountWithIntl( );
- const escapeKeyEvent = new KeyboardEvent('keydown', { keyCode: keyCodes.ESCAPE });
+ const escapeKeyEvent = new KeyboardEvent('keydown', { key: keys.ESCAPE });
document.dispatchEvent(escapeKeyEvent);
sinon.assert.calledOnce(onExitHandler);
diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js
index b4b18e086e809..168dddf0253d9 100644
--- a/src/legacy/ui/ui_render/ui_render_mixin.js
+++ b/src/legacy/ui/ui_render/ui_render_mixin.js
@@ -150,7 +150,23 @@ export function uiRenderMixin(kbnServer, server, config) {
]),
];
- const kpPluginIds = Array.from(kbnServer.newPlatform.__internals.uiPlugins.public.keys());
+ const kpUiPlugins = kbnServer.newPlatform.__internals.uiPlugins;
+ const kpPluginPublicPaths = new Map();
+ const kpPluginBundlePaths = new Set();
+
+ // recursively iterate over the kpUiPlugin ids and their required bundles
+ // to populate kpPluginPublicPaths and kpPluginBundlePaths
+ (function readKpPlugins(ids) {
+ for (const id of ids) {
+ if (kpPluginPublicPaths.has(id)) {
+ continue;
+ }
+
+ kpPluginPublicPaths.set(id, `${regularBundlePath}/plugin/${id}/`);
+ kpPluginBundlePaths.add(`${regularBundlePath}/plugin/${id}/${id}.plugin.js`);
+ readKpPlugins(kpUiPlugins.internal.get(id).requiredBundles);
+ }
+ })(kpUiPlugins.public.keys());
const jsDependencyPaths = [
...UiSharedDeps.jsDepFilenames.map(
@@ -160,9 +176,7 @@ export function uiRenderMixin(kbnServer, server, config) {
...(isCore ? [] : [`${dllBundlePath}/vendors_runtime.bundle.dll.js`, ...dllJsChunks]),
`${regularBundlePath}/core/core.entry.js`,
- ...kpPluginIds.map(
- (pluginId) => `${regularBundlePath}/plugin/${pluginId}/${pluginId}.plugin.js`
- ),
+ ...kpPluginBundlePaths,
];
// These paths should align with the bundle routes configured in
@@ -170,13 +184,7 @@ export function uiRenderMixin(kbnServer, server, config) {
const publicPathMap = JSON.stringify({
core: `${regularBundlePath}/core/`,
'kbn-ui-shared-deps': `${regularBundlePath}/kbn-ui-shared-deps/`,
- ...kpPluginIds.reduce(
- (acc, pluginId) => ({
- ...acc,
- [pluginId]: `${regularBundlePath}/plugin/${pluginId}/`,
- }),
- {}
- ),
+ ...Object.fromEntries(kpPluginPublicPaths),
});
const bootstrap = new AppBootstrap({
diff --git a/src/plugins/advanced_settings/kibana.json b/src/plugins/advanced_settings/kibana.json
index e6ca6e797ba45..8cf9b9c656d8f 100644
--- a/src/plugins/advanced_settings/kibana.json
+++ b/src/plugins/advanced_settings/kibana.json
@@ -3,5 +3,6 @@
"version": "kibana",
"server": true,
"ui": true,
- "requiredPlugins": ["management"]
+ "requiredPlugins": ["management"],
+ "requiredBundles": ["kibanaReact"]
}
diff --git a/src/plugins/advanced_settings/public/management_app/components/form/_form.scss b/src/plugins/advanced_settings/public/management_app/components/form/_form.scss
index 5fddaa178f580..8d768d200fdd2 100644
--- a/src/plugins/advanced_settings/public/management_app/components/form/_form.scss
+++ b/src/plugins/advanced_settings/public/management_app/components/form/_form.scss
@@ -1,4 +1,4 @@
-@import '@elastic/eui/src/components/header/variables';
+@import '@elastic/eui/src/global_styling/variables/header';
@import '@elastic/eui/src/components/nav_drawer/variables';
// TODO #64541
diff --git a/src/plugins/apm_oss/server/tutorial/index.ts b/src/plugins/apm_oss/server/tutorial/index.ts
index aa775d007de30..42609f7d75917 100644
--- a/src/plugins/apm_oss/server/tutorial/index.ts
+++ b/src/plugins/apm_oss/server/tutorial/index.ts
@@ -26,6 +26,7 @@ import { APM_STATIC_INDEX_PATTERN_ID } from '../../common/index_pattern_constant
const apmIntro = i18n.translate('apmOss.tutorial.introduction', {
defaultMessage: 'Collect in-depth performance metrics and errors from inside your applications.',
});
+const moduleName = 'apm';
export const tutorialProvider = ({
indexPatternTitle,
@@ -68,6 +69,7 @@ export const tutorialProvider = ({
name: i18n.translate('apmOss.tutorial.specProvider.name', {
defaultMessage: 'APM',
}),
+ moduleName,
category: TutorialsCategory.OTHER,
shortDescription: apmIntro,
longDescription: i18n.translate('apmOss.tutorial.specProvider.longDescription', {
diff --git a/src/plugins/bfetch/kibana.json b/src/plugins/bfetch/kibana.json
index 462d2f4b8bb7d..9f9f2176af671 100644
--- a/src/plugins/bfetch/kibana.json
+++ b/src/plugins/bfetch/kibana.json
@@ -2,5 +2,6 @@
"id": "bfetch",
"version": "kibana",
"server": true,
- "ui": true
+ "ui": true,
+ "requiredBundles": ["kibanaUtils"]
}
diff --git a/src/plugins/charts/kibana.json b/src/plugins/charts/kibana.json
index 9f4433e7099d8..c4643d541c31c 100644
--- a/src/plugins/charts/kibana.json
+++ b/src/plugins/charts/kibana.json
@@ -2,5 +2,6 @@
"id": "charts",
"version": "kibana",
"server": true,
- "ui": true
+ "ui": true,
+ "requiredBundles": ["kibanaUtils", "kibanaReact", "data"]
}
diff --git a/src/plugins/charts/public/services/colors/mock.ts b/src/plugins/charts/public/services/colors/mock.ts
index 924dbd6aa52a4..f88980e521dda 100644
--- a/src/plugins/charts/public/services/colors/mock.ts
+++ b/src/plugins/charts/public/services/colors/mock.ts
@@ -24,5 +24,5 @@ const colors = new ColorsService();
colors.init(coreMock.createSetup().uiSettings);
export const colorsServiceMock: ColorsService = {
- createColorLookupFunction: jest.fn(colors.createColorLookupFunction),
+ createColorLookupFunction: jest.fn(colors.createColorLookupFunction.bind(colors)),
} as any;
diff --git a/src/plugins/console/kibana.json b/src/plugins/console/kibana.json
index 57de385ba565c..031aa00eb6613 100644
--- a/src/plugins/console/kibana.json
+++ b/src/plugins/console/kibana.json
@@ -4,5 +4,6 @@
"server": true,
"ui": true,
"requiredPlugins": ["devTools", "home"],
- "optionalPlugins": ["usageCollection"]
+ "optionalPlugins": ["usageCollection"],
+ "requiredBundles": ["esUiShared", "kibanaReact", "kibanaUtils"]
}
diff --git a/src/plugins/console/public/application/components/editor_example.tsx b/src/plugins/console/public/application/components/editor_example.tsx
index e9e252e4ebb17..72a1056b1a866 100644
--- a/src/plugins/console/public/application/components/editor_example.tsx
+++ b/src/plugins/console/public/application/components/editor_example.tsx
@@ -18,8 +18,6 @@
*/
import { EuiScreenReaderOnly } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
-// @ts-ignore
-import exampleText from 'raw-loader!../constants/help_example.txt';
import React, { useEffect } from 'react';
import { createReadOnlyAceEditor } from '../models/legacy_core_editor';
@@ -27,6 +25,17 @@ interface EditorExampleProps {
panel: string;
}
+const exampleText = `
+# index a doc
+PUT index/1
+{
+ "body": "here"
+}
+
+# and get it ...
+GET index/1
+`;
+
export function EditorExample(props: EditorExampleProps) {
const elemId = `help-example-${props.panel}`;
const inputId = `help-example-${props.panel}-input`;
diff --git a/src/plugins/console/public/application/constants/help_example.txt b/src/plugins/console/public/application/constants/help_example.txt
deleted file mode 100644
index fd37c41367033..0000000000000
--- a/src/plugins/console/public/application/constants/help_example.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-# index a doc
-PUT index/1
-{
- "body": "here"
-}
-
-# and get it ...
-GET index/1
diff --git a/src/plugins/console/public/application/containers/console_history/console_history.tsx b/src/plugins/console/public/application/containers/console_history/console_history.tsx
index 8ec8b9c61bf03..433ad15990d77 100644
--- a/src/plugins/console/public/application/containers/console_history/console_history.tsx
+++ b/src/plugins/console/public/application/containers/console_history/console_history.tsx
@@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n';
import { memoize } from 'lodash';
import moment from 'moment';
import {
- keyCodes,
+ keys,
EuiSpacer,
EuiIcon,
EuiTitle,
@@ -125,17 +125,17 @@ export function ConsoleHistory({ close }: Props) {
{
- if (ev.keyCode === keyCodes.ENTER) {
+ if (ev.key === keys.ENTER) {
restoreRequestFromHistory(selectedReq.current);
return;
}
let currentIdx = selectedIndex;
- if (ev.keyCode === keyCodes.UP) {
+ if (ev.key === keys.ARROW_UP) {
ev.preventDefault();
--currentIdx;
- } else if (ev.keyCode === keyCodes.DOWN) {
+ } else if (ev.key === keys.ARROW_DOWN) {
ev.preventDefault();
++currentIdx;
}
diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx
index 6d4f532887cd9..880069d8ebc7a 100644
--- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx
+++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx
@@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n';
import { debounce } from 'lodash';
import { parse } from 'query-string';
import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react';
-import { useUIAceKeyboardMode } from '../../../../../../../es_ui_shared/public';
+import { ace } from '../../../../../../../es_ui_shared/public';
// @ts-ignore
import { retrieveAutoCompleteInfo, clearSubscriptions } from '../../../../../lib/mappings/mappings';
import { ConsoleMenu } from '../../../../components';
@@ -38,6 +38,8 @@ import { subscribeResizeChecker } from '../subscribe_console_resize_checker';
import { applyCurrentSettings } from './apply_editor_settings';
import { registerCommands } from './keyboard_shortcuts';
+const { useUIAceKeyboardMode } = ace;
+
export interface EditorProps {
initialTextValue: string;
}
diff --git a/src/plugins/console/public/styles/_app.scss b/src/plugins/console/public/styles/_app.scss
index c41df24912c2a..baf4cf1cbd143 100644
--- a/src/plugins/console/public/styles/_app.scss
+++ b/src/plugins/console/public/styles/_app.scss
@@ -1,5 +1,5 @@
// TODO: Move all of the styles here (should be modularised by, e.g., CSS-in-JS or CSS modules).
-@import '@elastic/eui/src/components/header/variables';
+@import '@elastic/eui/src/global_styling/variables/header';
// This value is calculated to static value using SCSS because calc in calc has issues in IE11
$headerHeightOffset: $euiHeaderHeightCompensation * 2;
diff --git a/src/plugins/dashboard/kibana.json b/src/plugins/dashboard/kibana.json
index 4cd8f3c7d981f..1b38c6d124fe1 100644
--- a/src/plugins/dashboard/kibana.json
+++ b/src/plugins/dashboard/kibana.json
@@ -12,5 +12,6 @@
],
"optionalPlugins": ["home", "share", "usageCollection"],
"server": true,
- "ui": true
+ "ui": true,
+ "requiredBundles": ["kibanaUtils", "kibanaReact", "home"]
}
diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx
index a321bc7959c5c..8138e1c7f4dfd 100644
--- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx
+++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx
@@ -60,6 +60,7 @@ import {
ViewMode,
ContainerOutput,
EmbeddableInput,
+ SavedObjectEmbeddableInput,
} from '../../../embeddable/public';
import { NavAction, SavedDashboardPanel } from '../types';
@@ -431,7 +432,7 @@ export class DashboardAppController {
.getIncomingEmbeddablePackage();
if (incomingState) {
if ('id' in incomingState) {
- container.addNewEmbeddable(incomingState.type, {
+ container.addOrUpdateEmbeddable(incomingState.type, {
savedObjectId: incomingState.id,
});
} else if ('input' in incomingState) {
@@ -440,7 +441,7 @@ export class DashboardAppController {
const explicitInput = {
savedVis: input,
};
- container.addNewEmbeddable(incomingState.type, explicitInput);
+ container.addOrUpdateEmbeddable(incomingState.type, explicitInput);
}
}
}
diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx
index f1ecd0f221926..ff74580ba256b 100644
--- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx
+++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx
@@ -46,7 +46,7 @@ import {
} from '../../../../kibana_react/public';
import { PLACEHOLDER_EMBEDDABLE } from './placeholder';
import { PanelPlacementMethod, IPanelPlacementArgs } from './panel/dashboard_panel_placement';
-import { EmbeddableStateTransfer } from '../../../../embeddable/public';
+import { EmbeddableStateTransfer, EmbeddableOutput } from '../../../../embeddable/public';
export interface DashboardContainerInput extends ContainerInput {
viewMode: ViewMode;
@@ -159,29 +159,55 @@ export class DashboardContainer extends Container) => {
- const finalPanels = { ...this.input.panels };
- delete finalPanels[placeholderPanelState.explicitInput.id];
- const newPanelId = newPanelState.explicitInput?.id
- ? newPanelState.explicitInput.id
- : uuid.v4();
- finalPanels[newPanelId] = {
- ...placeholderPanelState,
- ...newPanelState,
- gridData: {
- ...placeholderPanelState.gridData,
- i: newPanelId,
- },
+ newStateComplete.then((newPanelState: Partial) =>
+ this.replacePanel(placeholderPanelState, newPanelState)
+ );
+ }
+
+ public replacePanel(
+ previousPanelState: DashboardPanelState,
+ newPanelState: Partial
+ ) {
+ // TODO: In the current infrastructure, embeddables in a container do not react properly to
+ // changes. Removing the existing embeddable, and adding a new one is a temporary workaround
+ // until the container logic is fixed.
+ const finalPanels = { ...this.input.panels };
+ delete finalPanels[previousPanelState.explicitInput.id];
+ const newPanelId = newPanelState.explicitInput?.id ? newPanelState.explicitInput.id : uuid.v4();
+ finalPanels[newPanelId] = {
+ ...previousPanelState,
+ ...newPanelState,
+ gridData: {
+ ...previousPanelState.gridData,
+ i: newPanelId,
+ },
+ explicitInput: {
+ ...newPanelState.explicitInput,
+ id: newPanelId,
+ },
+ };
+ this.updateInput({
+ panels: finalPanels,
+ lastReloadRequestTime: new Date().getTime(),
+ });
+ }
+
+ public async addOrUpdateEmbeddable<
+ EEI extends EmbeddableInput = EmbeddableInput,
+ EEO extends EmbeddableOutput = EmbeddableOutput,
+ E extends IEmbeddable = IEmbeddable
+ >(type: string, explicitInput: Partial) {
+ if (explicitInput.id && this.input.panels[explicitInput.id]) {
+ this.replacePanel(this.input.panels[explicitInput.id], {
+ type,
explicitInput: {
- ...newPanelState.explicitInput,
- id: newPanelId,
+ ...explicitInput,
+ id: uuid.v4(),
},
- };
- this.updateInput({
- panels: finalPanels,
- lastReloadRequestTime: new Date().getTime(),
});
- });
+ } else {
+ this.addNewEmbeddable(type, explicitInput);
+ }
}
public render(dom: HTMLElement) {
diff --git a/src/plugins/data/common/constants.ts b/src/plugins/data/common/constants.ts
index 8ec72dc1f9a74..22db1552e4303 100644
--- a/src/plugins/data/common/constants.ts
+++ b/src/plugins/data/common/constants.ts
@@ -44,7 +44,8 @@ export const UI_SETTINGS = {
FORMAT_NUMBER_DEFAULT_LOCALE: 'format:number:defaultLocale',
TIMEPICKER_REFRESH_INTERVAL_DEFAULTS: 'timepicker:refreshIntervalDefaults',
TIMEPICKER_QUICK_RANGES: 'timepicker:quickRanges',
+ TIMEPICKER_TIME_DEFAULTS: 'timepicker:timeDefaults',
INDEXPATTERN_PLACEHOLDER: 'indexPattern:placeholder',
FILTERS_PINNED_BY_DEFAULT: 'filters:pinnedByDefault',
FILTERS_EDITOR_SUGGEST_VALUES: 'filterEditor:suggestValues',
-};
+} as const;
diff --git a/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts b/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts
index c1aa2efe46998..18048b81aab96 100644
--- a/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts
+++ b/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts
@@ -78,7 +78,12 @@ function decorateFlattenedWrapper(hit: Record, metaFields: Record {
+ constructor(private type: string, private savedObjectsClient: SavedObjectsClientContract) {}
+
+ public async unwrapAttributes(input: RefType | ValType): Promise {
+ if (isSavedObjectEmbeddableInput(input)) {
+ const savedObject: SimpleSavedObject = await this.savedObjectsClient.get<
+ SavedObjectAttributes
+ >(this.type, input.savedObjectId);
+ return savedObject.attributes;
+ }
+ return input.attributes;
+ }
+
+ public async wrapAttributes(
+ newAttributes: SavedObjectAttributes,
+ useRefType: boolean,
+ embeddable?: IEmbeddable
+ ): Promise> {
+ const savedObjectId =
+ embeddable && isSavedObjectEmbeddableInput(embeddable.getInput())
+ ? (embeddable.getInput() as SavedObjectEmbeddableInput).savedObjectId
+ : undefined;
+
+ if (useRefType) {
+ if (savedObjectId) {
+ await this.savedObjectsClient.update(this.type, savedObjectId, newAttributes);
+ return { savedObjectId } as RefType;
+ } else {
+ const savedItem = await this.savedObjectsClient.create(this.type, newAttributes);
+ return { savedObjectId: savedItem.id } as RefType;
+ }
+ } else {
+ return { attributes: newAttributes } as ValType;
+ }
+ }
+}
diff --git a/src/plugins/embeddable/public/lib/embeddables/index.ts b/src/plugins/embeddable/public/lib/embeddables/index.ts
index 5bab5ac27f3cc..06cb6e322acf3 100644
--- a/src/plugins/embeddable/public/lib/embeddables/index.ts
+++ b/src/plugins/embeddable/public/lib/embeddables/index.ts
@@ -25,4 +25,5 @@ export { ErrorEmbeddable, isErrorEmbeddable } from './error_embeddable';
export { withEmbeddableSubscription } from './with_subscription';
export { EmbeddableRoot } from './embeddable_root';
export * from './saved_object_embeddable';
+export { AttributeService } from './attribute_service';
export { EmbeddableRenderer, EmbeddableRendererProps } from './embeddable_renderer';
diff --git a/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts b/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts
index 6ca1800b16de4..5f093c55e94e4 100644
--- a/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts
+++ b/src/plugins/embeddable/public/lib/embeddables/saved_object_embeddable.ts
@@ -26,5 +26,5 @@ export interface SavedObjectEmbeddableInput extends EmbeddableInput {
export function isSavedObjectEmbeddableInput(
input: EmbeddableInput | SavedObjectEmbeddableInput
): input is SavedObjectEmbeddableInput {
- return (input as SavedObjectEmbeddableInput).savedObjectId !== undefined;
+ return Boolean((input as SavedObjectEmbeddableInput).savedObjectId);
}
diff --git a/src/plugins/embeddable/public/mocks.tsx b/src/plugins/embeddable/public/mocks.tsx
index efd0ccdc4553d..48e5483124704 100644
--- a/src/plugins/embeddable/public/mocks.tsx
+++ b/src/plugins/embeddable/public/mocks.tsx
@@ -99,6 +99,7 @@ const createStartContract = (): Start => {
getEmbeddableFactories: jest.fn(),
getEmbeddableFactory: jest.fn(),
EmbeddablePanel: jest.fn(),
+ getAttributeService: jest.fn(),
getEmbeddablePanel: jest.fn(),
getStateTransfer: jest.fn(() => createEmbeddableStateTransferMock() as EmbeddableStateTransfer),
filtersAndTimeRangeFromContext: jest.fn(),
diff --git a/src/plugins/embeddable/public/plugin.tsx b/src/plugins/embeddable/public/plugin.tsx
index 03bb4a4779267..508c82c4247ed 100644
--- a/src/plugins/embeddable/public/plugin.tsx
+++ b/src/plugins/embeddable/public/plugin.tsx
@@ -43,11 +43,13 @@ import {
defaultEmbeddableFactoryProvider,
IEmbeddable,
EmbeddablePanel,
+ SavedObjectEmbeddableInput,
ChartActionContext,
isRangeSelectTriggerContext,
isValueClickTriggerContext,
} from './lib';
import { EmbeddableFactoryDefinition } from './lib/embeddables/embeddable_factory_definition';
+import { AttributeService } from './lib/embeddables/attribute_service';
import { EmbeddableStateTransfer } from './lib/state_transfer';
export interface EmbeddableSetupDependencies {
@@ -82,6 +84,13 @@ export interface EmbeddableStart {
embeddableFactoryId: string
) => EmbeddableFactory | undefined;
getEmbeddableFactories: () => IterableIterator;
+ getAttributeService: <
+ A,
+ V extends EmbeddableInput & { attributes: A },
+ R extends SavedObjectEmbeddableInput
+ >(
+ type: string
+ ) => AttributeService;
/**
* Given {@link ChartActionContext} returns a list of `data` plugin {@link Filter} entries.
@@ -206,6 +215,7 @@ export class EmbeddablePublicPlugin implements Plugin new AttributeService(type, core.savedObjects.client),
filtersFromContext,
filtersAndTimeRangeFromContext,
getStateTransfer: (history?: ScopedHistory) => {
diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/ace/_ui_ace_keyboard_mode.scss b/src/plugins/es_ui_shared/__packages_do_not_import__/ace/_ui_ace_keyboard_mode.scss
new file mode 100644
index 0000000000000..5b637224c1784
--- /dev/null
+++ b/src/plugins/es_ui_shared/__packages_do_not_import__/ace/_ui_ace_keyboard_mode.scss
@@ -0,0 +1,24 @@
+.kbnUiAceKeyboardHint {
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ right: 0;
+ left: 0;
+ background: transparentize($euiColorEmptyShade, 0.3);
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+ opacity: 0;
+
+ &:focus {
+ opacity: 1;
+ border: 2px solid $euiColorPrimary;
+ z-index: $euiZLevel1;
+ }
+
+ &.kbnUiAceKeyboardHint-isInactive {
+ display: none;
+ }
+}
diff --git a/src/plugins/es_ui_shared/__packages_do_not_import__/ace/index.ts b/src/plugins/es_ui_shared/__packages_do_not_import__/ace/index.ts
new file mode 100644
index 0000000000000..72d0d6d85ee6e
--- /dev/null
+++ b/src/plugins/es_ui_shared/__packages_do_not_import__/ace/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export { useUIAceKeyboardMode } from './use_ui_ace_keyboard_mode';
diff --git a/src/plugins/es_ui_shared/public/use_ui_ace_keyboard_mode.tsx b/src/plugins/es_ui_shared/__packages_do_not_import__/ace/use_ui_ace_keyboard_mode.tsx
similarity index 95%
rename from src/plugins/es_ui_shared/public/use_ui_ace_keyboard_mode.tsx
rename to src/plugins/es_ui_shared/__packages_do_not_import__/ace/use_ui_ace_keyboard_mode.tsx
index a93906d50b64a..d0d1aa1d8db15 100644
--- a/src/plugins/es_ui_shared/public/use_ui_ace_keyboard_mode.tsx
+++ b/src/plugins/es_ui_shared/__packages_do_not_import__/ace/use_ui_ace_keyboard_mode.tsx
@@ -16,9 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
+
import React, { useEffect, useRef } from 'react';
import * as ReactDOM from 'react-dom';
-import { keyCodes, EuiText } from '@elastic/eui';
+import { keys, EuiText } from '@elastic/eui';
+
+import './_ui_ace_keyboard_mode.scss';
const OverlayText = () => (
// The point of this element is for accessibility purposes, so ignore eslint error
@@ -37,7 +40,7 @@ export function useUIAceKeyboardMode(aceTextAreaElement: HTMLTextAreaElement | n
useEffect(() => {
function onDismissOverlay(event: KeyboardEvent) {
- if (event.keyCode === keyCodes.ENTER) {
+ if (event.key === keys.ENTER) {
event.preventDefault();
aceTextAreaElement!.focus();
}
@@ -63,7 +66,7 @@ export function useUIAceKeyboardMode(aceTextAreaElement: HTMLTextAreaElement | n
};
const aceKeydownListener = (event: KeyboardEvent) => {
- if (event.keyCode === keyCodes.ESCAPE && !autoCompleteVisibleRef.current) {
+ if (event.key === keys.ESCAPE && !autoCompleteVisibleRef.current) {
event.preventDefault();
event.stopPropagation();
enableOverlay();
diff --git a/src/plugins/es_ui_shared/kibana.json b/src/plugins/es_ui_shared/kibana.json
index 980f43ea46a68..eab7355d66f09 100644
--- a/src/plugins/es_ui_shared/kibana.json
+++ b/src/plugins/es_ui_shared/kibana.json
@@ -10,5 +10,8 @@
"static/forms/helpers",
"static/forms/components",
"static/forms/helpers/field_validators/types"
+ ],
+ "requiredBundles": [
+ "data"
]
}
diff --git a/src/plugins/es_ui_shared/public/ace/index.ts b/src/plugins/es_ui_shared/public/ace/index.ts
new file mode 100644
index 0000000000000..98507fa2fd6ad
--- /dev/null
+++ b/src/plugins/es_ui_shared/public/ace/index.ts
@@ -0,0 +1,20 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export { useUIAceKeyboardMode } from '../../__packages_do_not_import__/ace';
diff --git a/src/plugins/es_ui_shared/public/index.ts b/src/plugins/es_ui_shared/public/index.ts
index d472b7e462057..98a305fe68f08 100644
--- a/src/plugins/es_ui_shared/public/index.ts
+++ b/src/plugins/es_ui_shared/public/index.ts
@@ -23,6 +23,7 @@
*/
import * as Forms from './forms';
import * as Monaco from './monaco';
+import * as ace from './ace';
export { JsonEditor, OnJsonEditorUpdateHandler } from './components/json_editor';
@@ -41,8 +42,6 @@ export {
export { indices } from './indices';
-export { useUIAceKeyboardMode } from './use_ui_ace_keyboard_mode';
-
export {
installXJsonMode,
XJsonMode,
@@ -66,7 +65,7 @@ export {
useAuthorizationContext,
} from './authorization';
-export { Monaco, Forms };
+export { Monaco, Forms, ace };
export { extractQueryParams } from './url';
diff --git a/src/plugins/es_ui_shared/static/forms/components/form_row.tsx b/src/plugins/es_ui_shared/static/forms/components/form_row.tsx
index ad5a517e40cfb..d38e6c4f5fd95 100644
--- a/src/plugins/es_ui_shared/static/forms/components/form_row.tsx
+++ b/src/plugins/es_ui_shared/static/forms/components/form_row.tsx
@@ -57,13 +57,9 @@ export const FormRow = ({
titleWrapped = title;
}
- if (!children && !field) {
- throw new Error('You need to provide either children or a field to the FormRow');
- }
-
return (
- {children ? children : }
+ {children ? children : field ? : null}
);
};
diff --git a/src/plugins/expressions/kibana.json b/src/plugins/expressions/kibana.json
index 4774c69cc29ff..5163331088103 100644
--- a/src/plugins/expressions/kibana.json
+++ b/src/plugins/expressions/kibana.json
@@ -6,5 +6,10 @@
"requiredPlugins": [
"bfetch"
],
- "extraPublicDirs": ["common", "common/fonts"]
+ "extraPublicDirs": ["common", "common/fonts"],
+ "requiredBundles": [
+ "kibanaUtils",
+ "inspector",
+ "data"
+ ]
}
diff --git a/src/plugins/home/kibana.json b/src/plugins/home/kibana.json
index 1c4b44a946e62..74bd3625ca964 100644
--- a/src/plugins/home/kibana.json
+++ b/src/plugins/home/kibana.json
@@ -4,5 +4,8 @@
"server": true,
"ui": true,
"requiredPlugins": ["data", "kibanaLegacy"],
- "optionalPlugins": ["usageCollection", "telemetry"]
+ "optionalPlugins": ["usageCollection", "telemetry"],
+ "requiredBundles": [
+ "kibanaReact"
+ ]
}
diff --git a/src/plugins/home/public/application/application.tsx b/src/plugins/home/public/application/application.tsx
index 3729e4e2aa089..627bd10d7c2c8 100644
--- a/src/plugins/home/public/application/application.tsx
+++ b/src/plugins/home/public/application/application.tsx
@@ -20,14 +20,19 @@
import React from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import { i18n } from '@kbn/i18n';
-import { ScopedHistory } from 'kibana/public';
+import { ScopedHistory, CoreStart } from 'kibana/public';
+import { KibanaContextProvider } from '../../../kibana_react/public';
// @ts-ignore
import { HomeApp } from './components/home_app';
import { getServices } from './kibana_services';
import './index.scss';
-export const renderApp = async (element: HTMLElement, history: ScopedHistory) => {
+export const renderApp = async (
+ element: HTMLElement,
+ coreStart: CoreStart,
+ history: ScopedHistory
+) => {
const homeTitle = i18n.translate('home.breadcrumbs.homeTitle', { defaultMessage: 'Home' });
const { featureCatalogue, chrome } = getServices();
@@ -36,7 +41,12 @@ export const renderApp = async (element: HTMLElement, history: ScopedHistory) =>
chrome.setBreadcrumbs([{ text: homeTitle }]);
- render( , element);
+ render(
+
+
+ ,
+ element
+ );
// dispatch synthetic hash change event to update hash history objects
// this is necessary because hash updates triggered by using popState won't trigger this event naturally.
diff --git a/src/plugins/home/public/application/components/tutorial/tutorial.js b/src/plugins/home/public/application/components/tutorial/tutorial.js
index 576f732278b8e..8139bc6d38ab1 100644
--- a/src/plugins/home/public/application/components/tutorial/tutorial.js
+++ b/src/plugins/home/public/application/components/tutorial/tutorial.js
@@ -334,6 +334,23 @@ class TutorialUi extends React.Component {
}
};
+ renderModuleNotices() {
+ const notices = getServices().tutorialService.getModuleNotices();
+ if (notices.length && this.state.tutorial.moduleName) {
+ return (
+
+ {notices.map((ModuleNotice, index) => (
+
+
+
+ ))}
+
+ );
+ } else {
+ return null;
+ }
+ }
+
render() {
let content;
if (this.state.notFound) {
@@ -382,6 +399,7 @@ class TutorialUi extends React.Component {
isBeta={this.state.tutorial.isBeta}
/>
+ {this.renderModuleNotices()}
{this.renderInstructionSetsToggle()}
diff --git a/src/plugins/home/public/application/components/tutorial/tutorial.test.js b/src/plugins/home/public/application/components/tutorial/tutorial.test.js
index 23b0dc50018c1..9944ac4848bc6 100644
--- a/src/plugins/home/public/application/components/tutorial/tutorial.test.js
+++ b/src/plugins/home/public/application/components/tutorial/tutorial.test.js
@@ -28,6 +28,9 @@ jest.mock('../../kibana_services', () => ({
chrome: {
setBreadcrumbs: () => {},
},
+ tutorialService: {
+ getModuleNotices: () => [],
+ },
}),
}));
jest.mock('../../../../../kibana_react/public', () => {
diff --git a/src/plugins/home/public/application/components/tutorial_directory.js b/src/plugins/home/public/application/components/tutorial_directory.js
index 774b23af11ac8..948024ae85dda 100644
--- a/src/plugins/home/public/application/components/tutorial_directory.js
+++ b/src/plugins/home/public/application/components/tutorial_directory.js
@@ -30,6 +30,7 @@ import {
EuiTab,
EuiFlexItem,
EuiFlexGrid,
+ EuiFlexGroup,
EuiSpacer,
EuiTitle,
EuiPageBody,
@@ -102,6 +103,7 @@ class TutorialDirectoryUi extends React.Component {
this.state = {
selectedTabId: openTab,
tutorialCards: [],
+ notices: getServices().tutorialService.getDirectoryNotices(),
};
}
@@ -227,18 +229,62 @@ class TutorialDirectoryUi extends React.Component {
);
};
+ renderNotices = () => {
+ const notices = getServices().tutorialService.getDirectoryNotices();
+ return notices.length ? (
+
+ {notices.map((DirectoryNotice, index) => (
+
+
+
+ ))}
+
+ ) : null;
+ };
+
+ renderHeaderLinks = () => {
+ const headerLinks = getServices().tutorialService.getDirectoryHeaderLinks();
+ return headerLinks.length ? (
+
+ {headerLinks.map((HeaderLink, index) => (
+
+
+
+ ))}
+
+ ) : null;
+ };
+
+ renderHeader = () => {
+ const notices = this.renderNotices();
+ const headerLinks = this.renderHeaderLinks();
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ {headerLinks ? {headerLinks} : null}
+
+ {notices}
+ >
+ );
+ };
+
render() {
return (
-
-
-
-
-
-
+ {this.renderHeader()}
-
{this.renderTabs()}
{this.renderTabContent()}
diff --git a/src/plugins/home/public/index.ts b/src/plugins/home/public/index.ts
index 587dbe886d505..dc48332e052de 100644
--- a/src/plugins/home/public/index.ts
+++ b/src/plugins/home/public/index.ts
@@ -30,6 +30,9 @@ export {
FeatureCatalogueCategory,
Environment,
TutorialVariables,
+ TutorialDirectoryNoticeComponent,
+ TutorialDirectoryHeaderLinkComponent,
+ TutorialModuleNoticeComponent,
} from './services';
export * from '../common/instruction_variant';
import { HomePublicPlugin } from './plugin';
diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts
index d05fce652bd40..6859d916a61af 100644
--- a/src/plugins/home/public/plugin.ts
+++ b/src/plugins/home/public/plugin.ts
@@ -104,7 +104,7 @@ export class HomePublicPlugin
i18n.translate('home.pageTitle', { defaultMessage: 'Home' })
);
const { renderApp } = await import('./application');
- return await renderApp(params.element, params.history);
+ return await renderApp(params.element, coreStart, params.history);
},
});
kibanaLegacy.forwardApp('home', 'home');
diff --git a/src/plugins/home/public/services/tutorials/index.ts b/src/plugins/home/public/services/tutorials/index.ts
index 3de1e67204d96..44f0badd531b7 100644
--- a/src/plugins/home/public/services/tutorials/index.ts
+++ b/src/plugins/home/public/services/tutorials/index.ts
@@ -17,4 +17,11 @@
* under the License.
*/
-export { TutorialService, TutorialVariables, TutorialServiceSetup } from './tutorial_service';
+export {
+ TutorialService,
+ TutorialVariables,
+ TutorialServiceSetup,
+ TutorialDirectoryNoticeComponent,
+ TutorialDirectoryHeaderLinkComponent,
+ TutorialModuleNoticeComponent,
+} from './tutorial_service';
diff --git a/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts b/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts
index bd604fb231dee..667730e25a2e3 100644
--- a/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts
+++ b/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts
@@ -22,6 +22,9 @@ import { TutorialService, TutorialServiceSetup } from './tutorial_service';
const createSetupMock = (): jest.Mocked => {
const setup = {
setVariable: jest.fn(),
+ registerDirectoryNotice: jest.fn(),
+ registerDirectoryHeaderLink: jest.fn(),
+ registerModuleNotice: jest.fn(),
};
return setup;
};
@@ -30,6 +33,9 @@ const createMock = (): jest.Mocked> => {
const service = {
setup: jest.fn(),
getVariables: jest.fn(() => ({})),
+ getDirectoryNotices: jest.fn(() => []),
+ getDirectoryHeaderLinks: jest.fn(() => []),
+ getModuleNotices: jest.fn(() => []),
};
service.setup.mockImplementation(createSetupMock);
return service;
diff --git a/src/plugins/home/public/services/tutorials/tutorial_service.test.ts b/src/plugins/home/public/services/tutorials/tutorial_service.test.ts
deleted file mode 100644
index f4bcd71a39e8a..0000000000000
--- a/src/plugins/home/public/services/tutorials/tutorial_service.test.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Licensed to Elasticsearch B.V. under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch B.V. licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
- * not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { TutorialService } from './tutorial_service';
-
-describe('TutorialService', () => {
- describe('setup', () => {
- test('allows multiple set calls', () => {
- const setup = new TutorialService().setup();
- expect(() => {
- setup.setVariable('abc', 123);
- setup.setVariable('def', 456);
- }).not.toThrow();
- });
-
- test('throws when same variable is set twice', () => {
- const setup = new TutorialService().setup();
- expect(() => {
- setup.setVariable('abc', 123);
- setup.setVariable('abc', 456);
- }).toThrow();
- });
- });
-
- describe('getVariables', () => {
- test('returns empty object', () => {
- const service = new TutorialService();
- expect(service.getVariables()).toEqual({});
- });
-
- test('returns last state of update calls', () => {
- const service = new TutorialService();
- const setup = service.setup();
- setup.setVariable('abc', 123);
- setup.setVariable('def', { subKey: 456 });
- expect(service.getVariables()).toEqual({ abc: 123, def: { subKey: 456 } });
- });
- });
-});
diff --git a/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx b/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx
new file mode 100644
index 0000000000000..2a60550e39d90
--- /dev/null
+++ b/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx
@@ -0,0 +1,151 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React from 'react';
+import { TutorialService } from './tutorial_service';
+
+describe('TutorialService', () => {
+ describe('setup', () => {
+ test('allows multiple set variable calls', () => {
+ const setup = new TutorialService().setup();
+ expect(() => {
+ setup.setVariable('abc', 123);
+ setup.setVariable('def', 456);
+ }).not.toThrow();
+ });
+
+ test('throws when same variable is set twice', () => {
+ const setup = new TutorialService().setup();
+ expect(() => {
+ setup.setVariable('abc', 123);
+ setup.setVariable('abc', 456);
+ }).toThrow();
+ });
+
+ test('allows multiple register directory notice calls', () => {
+ const setup = new TutorialService().setup();
+ expect(() => {
+ setup.registerDirectoryNotice('abc', () =>
);
+ setup.registerDirectoryNotice('def', () => );
+ }).not.toThrow();
+ });
+
+ test('throws when same directory notice is registered twice', () => {
+ const setup = new TutorialService().setup();
+ expect(() => {
+ setup.registerDirectoryNotice('abc', () =>
);
+ setup.registerDirectoryNotice('abc', () => );
+ }).toThrow();
+ });
+
+ test('allows multiple register directory header link calls', () => {
+ const setup = new TutorialService().setup();
+ expect(() => {
+ setup.registerDirectoryHeaderLink('abc', () => 123 );
+ setup.registerDirectoryHeaderLink('def', () => 456 );
+ }).not.toThrow();
+ });
+
+ test('throws when same directory header link is registered twice', () => {
+ const setup = new TutorialService().setup();
+ expect(() => {
+ setup.registerDirectoryHeaderLink('abc', () => 123 );
+ setup.registerDirectoryHeaderLink('abc', () => 456 );
+ }).toThrow();
+ });
+
+ test('allows multiple register module notice calls', () => {
+ const setup = new TutorialService().setup();
+ expect(() => {
+ setup.registerModuleNotice('abc', () =>
);
+ setup.registerModuleNotice('def', () => );
+ }).not.toThrow();
+ });
+
+ test('throws when same module notice is registered twice', () => {
+ const setup = new TutorialService().setup();
+ expect(() => {
+ setup.registerModuleNotice('abc', () =>
);
+ setup.registerModuleNotice('abc', () => );
+ }).toThrow();
+ });
+ });
+
+ describe('getVariables', () => {
+ test('returns empty object', () => {
+ const service = new TutorialService();
+ expect(service.getVariables()).toEqual({});
+ });
+
+ test('returns last state of update calls', () => {
+ const service = new TutorialService();
+ const setup = service.setup();
+ setup.setVariable('abc', 123);
+ setup.setVariable('def', { subKey: 456 });
+ expect(service.getVariables()).toEqual({ abc: 123, def: { subKey: 456 } });
+ });
+ });
+
+ describe('getDirectoryNotices', () => {
+ test('returns empty array', () => {
+ const service = new TutorialService();
+ expect(service.getDirectoryNotices()).toEqual([]);
+ });
+
+ test('returns last state of register calls', () => {
+ const service = new TutorialService();
+ const setup = service.setup();
+ const notices = [() =>
, () => ];
+ setup.registerDirectoryNotice('abc', notices[0]);
+ setup.registerDirectoryNotice('def', notices[1]);
+ expect(service.getDirectoryNotices()).toEqual(notices);
+ });
+ });
+
+ describe('getDirectoryHeaderLinks', () => {
+ test('returns empty array', () => {
+ const service = new TutorialService();
+ expect(service.getDirectoryHeaderLinks()).toEqual([]);
+ });
+
+ test('returns last state of register calls', () => {
+ const service = new TutorialService();
+ const setup = service.setup();
+ const links = [() => 123 , () => 456 ];
+ setup.registerDirectoryHeaderLink('abc', links[0]);
+ setup.registerDirectoryHeaderLink('def', links[1]);
+ expect(service.getDirectoryHeaderLinks()).toEqual(links);
+ });
+ });
+
+ describe('getModuleNotices', () => {
+ test('returns empty array', () => {
+ const service = new TutorialService();
+ expect(service.getModuleNotices()).toEqual([]);
+ });
+
+ test('returns last state of register calls', () => {
+ const service = new TutorialService();
+ const setup = service.setup();
+ const notices = [() =>
, () => ];
+ setup.registerModuleNotice('abc', notices[0]);
+ setup.registerModuleNotice('def', notices[1]);
+ expect(service.getModuleNotices()).toEqual(notices);
+ });
+ });
+});
diff --git a/src/plugins/home/public/services/tutorials/tutorial_service.ts b/src/plugins/home/public/services/tutorials/tutorial_service.ts
index 38297a6437315..538cea1c70458 100644
--- a/src/plugins/home/public/services/tutorials/tutorial_service.ts
+++ b/src/plugins/home/public/services/tutorials/tutorial_service.ts
@@ -16,12 +16,29 @@
* specific language governing permissions and limitations
* under the License.
*/
+import React from 'react';
/** @public */
export type TutorialVariables = Partial>;
+/** @public */
+export type TutorialDirectoryNoticeComponent = React.FC;
+
+/** @public */
+export type TutorialDirectoryHeaderLinkComponent = React.FC;
+
+/** @public */
+export type TutorialModuleNoticeComponent = React.FC<{
+ moduleName: string;
+}>;
+
export class TutorialService {
private tutorialVariables: TutorialVariables = {};
+ private tutorialDirectoryNotices: { [key: string]: TutorialDirectoryNoticeComponent } = {};
+ private tutorialDirectoryHeaderLinks: {
+ [key: string]: TutorialDirectoryHeaderLinkComponent;
+ } = {};
+ private tutorialModuleNotices: { [key: string]: TutorialModuleNoticeComponent } = {};
public setup() {
return {
@@ -34,12 +51,57 @@ export class TutorialService {
}
this.tutorialVariables[key] = value;
},
+
+ /**
+ * Registers a component that will be rendered at the top of tutorial directory page.
+ */
+ registerDirectoryNotice: (id: string, component: TutorialDirectoryNoticeComponent) => {
+ if (this.tutorialDirectoryNotices[id]) {
+ throw new Error(`directory notice ${id} already set`);
+ }
+ this.tutorialDirectoryNotices[id] = component;
+ },
+
+ /**
+ * Registers a component that will be rendered next to tutorial directory title/header area.
+ */
+ registerDirectoryHeaderLink: (
+ id: string,
+ component: TutorialDirectoryHeaderLinkComponent
+ ) => {
+ if (this.tutorialDirectoryHeaderLinks[id]) {
+ throw new Error(`directory header link ${id} already set`);
+ }
+ this.tutorialDirectoryHeaderLinks[id] = component;
+ },
+
+ /**
+ * Registers a component that will be rendered in the description of a tutorial that is associated with a module.
+ */
+ registerModuleNotice: (id: string, component: TutorialModuleNoticeComponent) => {
+ if (this.tutorialModuleNotices[id]) {
+ throw new Error(`module notice ${id} already set`);
+ }
+ this.tutorialModuleNotices[id] = component;
+ },
};
}
public getVariables() {
return this.tutorialVariables;
}
+
+ public getDirectoryNotices() {
+ return Object.values(this.tutorialDirectoryNotices);
+ }
+
+ public getDirectoryHeaderLinks() {
+ return Object.values(this.tutorialDirectoryHeaderLinks);
+ }
+
+ public getModuleNotices() {
+ return Object.values(this.tutorialModuleNotices);
+ }
}
export type TutorialServiceSetup = ReturnType;
diff --git a/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts b/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts
index 32e5483b8b070..bf28212624a4d 100644
--- a/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts
+++ b/src/plugins/home/server/services/tutorials/lib/tutorial_schema.ts
@@ -110,6 +110,7 @@ export const tutorialSchema = {
.required(),
category: Joi.string().valid(Object.values(TUTORIAL_CATEGORY)).required(),
name: Joi.string().required(),
+ moduleName: Joi.string(),
isBeta: Joi.boolean().default(false),
shortDescription: Joi.string().required(),
euiIconType: Joi.string(), // EUI icon type string, one of https://elastic.github.io/eui/#/icons
diff --git a/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts b/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts
index 210d563696667..a6b70cd70c02d 100644
--- a/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts
+++ b/src/plugins/home/server/services/tutorials/lib/tutorials_registry_types.ts
@@ -80,6 +80,7 @@ export interface TutorialSchema {
id: string;
category: TutorialsCategory;
name: string;
+ moduleName?: string;
isBeta?: boolean;
shortDescription: string;
euiIconType?: IconType; // EUI icon type string, one of https://elastic.github.io/eui/#/display/icons;
diff --git a/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts b/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts
index 8144fef2d92e4..b91a265da7d18 100644
--- a/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts
+++ b/src/plugins/home/server/services/tutorials/tutorials_registry.test.ts
@@ -54,6 +54,7 @@ const VALID_TUTORIAL: TutorialSchema = {
id: 'test',
category: 'logging' as TutorialsCategory,
name: 'new tutorial provider',
+ moduleName: 'test',
isBeta: false,
shortDescription: 'short description',
euiIconType: 'alert',
diff --git a/src/plugins/home/server/tutorials/activemq_logs/index.ts b/src/plugins/home/server/tutorials/activemq_logs/index.ts
index e85100996d4a1..c11c070637ae1 100644
--- a/src/plugins/home/server/tutorials/activemq_logs/index.ts
+++ b/src/plugins/home/server/tutorials/activemq_logs/index.ts
@@ -37,6 +37,7 @@ export function activemqLogsSpecProvider(context: TutorialContext): TutorialSche
name: i18n.translate('home.tutorials.activemqLogs.nameTitle', {
defaultMessage: 'ActiveMQ logs',
}),
+ moduleName,
category: TutorialsCategory.LOGGING,
shortDescription: i18n.translate('home.tutorials.activemqLogs.shortDescription', {
defaultMessage: 'Collect ActiveMQ logs with Filebeat.',
diff --git a/src/plugins/home/server/tutorials/activemq_metrics/index.ts b/src/plugins/home/server/tutorials/activemq_metrics/index.ts
index 088c5db4c6137..e00ffb4773bea 100644
--- a/src/plugins/home/server/tutorials/activemq_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/activemq_metrics/index.ts
@@ -36,6 +36,7 @@ export function activemqMetricsSpecProvider(context: TutorialContext): TutorialS
name: i18n.translate('home.tutorials.activemqMetrics.nameTitle', {
defaultMessage: 'ActiveMQ metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.activemqMetrics.shortDescription', {
defaultMessage: 'Fetch monitoring metrics from ActiveMQ instances.',
diff --git a/src/plugins/home/server/tutorials/aerospike_metrics/index.ts b/src/plugins/home/server/tutorials/aerospike_metrics/index.ts
index 58ab2dcf0986f..c65022c1875c4 100644
--- a/src/plugins/home/server/tutorials/aerospike_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/aerospike_metrics/index.ts
@@ -36,6 +36,7 @@ export function aerospikeMetricsSpecProvider(context: TutorialContext): Tutorial
name: i18n.translate('home.tutorials.aerospikeMetrics.nameTitle', {
defaultMessage: 'Aerospike metrics',
}),
+ moduleName,
isBeta: false,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.aerospikeMetrics.shortDescription', {
diff --git a/src/plugins/home/server/tutorials/apache_logs/index.ts b/src/plugins/home/server/tutorials/apache_logs/index.ts
index 434f0b0b83f98..94fa9ad1258ec 100644
--- a/src/plugins/home/server/tutorials/apache_logs/index.ts
+++ b/src/plugins/home/server/tutorials/apache_logs/index.ts
@@ -37,6 +37,7 @@ export function apacheLogsSpecProvider(context: TutorialContext): TutorialSchema
name: i18n.translate('home.tutorials.apacheLogs.nameTitle', {
defaultMessage: 'Apache logs',
}),
+ moduleName,
category: TutorialsCategory.LOGGING,
shortDescription: i18n.translate('home.tutorials.apacheLogs.shortDescription', {
defaultMessage: 'Collect and parse access and error logs created by the Apache HTTP server.',
diff --git a/src/plugins/home/server/tutorials/apache_metrics/index.ts b/src/plugins/home/server/tutorials/apache_metrics/index.ts
index 1521c9820c400..91de90b9f6c6b 100644
--- a/src/plugins/home/server/tutorials/apache_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/apache_metrics/index.ts
@@ -36,6 +36,7 @@ export function apacheMetricsSpecProvider(context: TutorialContext): TutorialSch
name: i18n.translate('home.tutorials.apacheMetrics.nameTitle', {
defaultMessage: 'Apache metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.apacheMetrics.shortDescription', {
defaultMessage: 'Fetch internal metrics from the Apache 2 HTTP server.',
diff --git a/src/plugins/home/server/tutorials/auditbeat/index.ts b/src/plugins/home/server/tutorials/auditbeat/index.ts
index 214fda5a7cc53..44a97bfce6cef 100644
--- a/src/plugins/home/server/tutorials/auditbeat/index.ts
+++ b/src/plugins/home/server/tutorials/auditbeat/index.ts
@@ -31,11 +31,13 @@ import {
export function auditbeatSpecProvider(context: TutorialContext): TutorialSchema {
const platforms = ['OSX', 'DEB', 'RPM', 'WINDOWS'] as const;
+ const moduleName = 'auditbeat';
return {
id: 'auditbeat',
name: i18n.translate('home.tutorials.auditbeat.nameTitle', {
defaultMessage: 'Auditbeat',
}),
+ moduleName,
category: TutorialsCategory.SECURITY_SOLUTION,
shortDescription: i18n.translate('home.tutorials.auditbeat.shortDescription', {
defaultMessage: 'Collect audit data from your hosts.',
diff --git a/src/plugins/home/server/tutorials/aws_logs/index.ts b/src/plugins/home/server/tutorials/aws_logs/index.ts
index 2fa22fa2c2d70..b875d93952c7a 100644
--- a/src/plugins/home/server/tutorials/aws_logs/index.ts
+++ b/src/plugins/home/server/tutorials/aws_logs/index.ts
@@ -37,6 +37,7 @@ export function awsLogsSpecProvider(context: TutorialContext): TutorialSchema {
name: i18n.translate('home.tutorials.awsLogs.nameTitle', {
defaultMessage: 'AWS S3 based logs',
}),
+ moduleName,
category: TutorialsCategory.LOGGING,
shortDescription: i18n.translate('home.tutorials.awsLogs.shortDescription', {
defaultMessage: 'Collect AWS logs from S3 bucket with Filebeat.',
diff --git a/src/plugins/home/server/tutorials/aws_metrics/index.ts b/src/plugins/home/server/tutorials/aws_metrics/index.ts
index c52620e150b5f..549e98280bef2 100644
--- a/src/plugins/home/server/tutorials/aws_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/aws_metrics/index.ts
@@ -36,6 +36,7 @@ export function awsMetricsSpecProvider(context: TutorialContext): TutorialSchema
name: i18n.translate('home.tutorials.awsMetrics.nameTitle', {
defaultMessage: 'AWS metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.awsMetrics.shortDescription', {
defaultMessage:
diff --git a/src/plugins/home/server/tutorials/azure_logs/index.ts b/src/plugins/home/server/tutorials/azure_logs/index.ts
index 06aef411775f1..3624bea96b465 100644
--- a/src/plugins/home/server/tutorials/azure_logs/index.ts
+++ b/src/plugins/home/server/tutorials/azure_logs/index.ts
@@ -37,6 +37,7 @@ export function azureLogsSpecProvider(context: TutorialContext): TutorialSchema
name: i18n.translate('home.tutorials.azureLogs.nameTitle', {
defaultMessage: 'Azure logs',
}),
+ moduleName,
isBeta: true,
category: TutorialsCategory.LOGGING,
shortDescription: i18n.translate('home.tutorials.azureLogs.shortDescription', {
diff --git a/src/plugins/home/server/tutorials/azure_metrics/index.ts b/src/plugins/home/server/tutorials/azure_metrics/index.ts
index c11b3ac0139ba..ac92d70fc64f5 100644
--- a/src/plugins/home/server/tutorials/azure_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/azure_metrics/index.ts
@@ -36,6 +36,7 @@ export function azureMetricsSpecProvider(context: TutorialContext): TutorialSche
name: i18n.translate('home.tutorials.azureMetrics.nameTitle', {
defaultMessage: 'Azure metrics',
}),
+ moduleName,
isBeta: false,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.azureMetrics.shortDescription', {
diff --git a/src/plugins/home/server/tutorials/ceph_metrics/index.ts b/src/plugins/home/server/tutorials/ceph_metrics/index.ts
index 968a0a3f66b0a..71e540454bc3a 100644
--- a/src/plugins/home/server/tutorials/ceph_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/ceph_metrics/index.ts
@@ -36,6 +36,7 @@ export function cephMetricsSpecProvider(context: TutorialContext): TutorialSchem
name: i18n.translate('home.tutorials.cephMetrics.nameTitle', {
defaultMessage: 'Ceph metrics',
}),
+ moduleName,
isBeta: false,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.cephMetrics.shortDescription', {
diff --git a/src/plugins/home/server/tutorials/cisco_logs/index.ts b/src/plugins/home/server/tutorials/cisco_logs/index.ts
index 2322f503b80ce..b771744a069c3 100644
--- a/src/plugins/home/server/tutorials/cisco_logs/index.ts
+++ b/src/plugins/home/server/tutorials/cisco_logs/index.ts
@@ -37,6 +37,7 @@ export function ciscoLogsSpecProvider(context: TutorialContext): TutorialSchema
name: i18n.translate('home.tutorials.ciscoLogs.nameTitle', {
defaultMessage: 'Cisco',
}),
+ moduleName,
category: TutorialsCategory.SECURITY_SOLUTION,
shortDescription: i18n.translate('home.tutorials.ciscoLogs.shortDescription', {
defaultMessage: 'Collect and parse logs received from Cisco ASA firewalls.',
diff --git a/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts b/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts
index 9d33d9bf786d0..fb7b07c5dc1af 100644
--- a/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts
+++ b/src/plugins/home/server/tutorials/cloudwatch_logs/index.ts
@@ -30,11 +30,13 @@ import {
} from '../../services/tutorials/lib/tutorials_registry_types';
export function cloudwatchLogsSpecProvider(context: TutorialContext): TutorialSchema {
+ const moduleName = 'aws';
return {
id: 'cloudwatchLogs',
name: i18n.translate('home.tutorials.cloudwatchLogs.nameTitle', {
defaultMessage: 'AWS Cloudwatch logs',
}),
+ moduleName,
category: TutorialsCategory.LOGGING,
shortDescription: i18n.translate('home.tutorials.cloudwatchLogs.shortDescription', {
defaultMessage: 'Collect Cloudwatch logs with Functionbeat.',
diff --git a/src/plugins/home/server/tutorials/cockroachdb_metrics/index.ts b/src/plugins/home/server/tutorials/cockroachdb_metrics/index.ts
index 96c02f24e347a..1cb318c83bd34 100644
--- a/src/plugins/home/server/tutorials/cockroachdb_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/cockroachdb_metrics/index.ts
@@ -36,6 +36,7 @@ export function cockroachdbMetricsSpecProvider(context: TutorialContext): Tutori
name: i18n.translate('home.tutorials.cockroachdbMetrics.nameTitle', {
defaultMessage: 'CockroachDB metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.cockroachdbMetrics.shortDescription', {
defaultMessage: 'Fetch monitoring metrics from the CockroachDB server.',
diff --git a/src/plugins/home/server/tutorials/consul_metrics/index.ts b/src/plugins/home/server/tutorials/consul_metrics/index.ts
index 8bf4333cb018f..e389db502a769 100644
--- a/src/plugins/home/server/tutorials/consul_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/consul_metrics/index.ts
@@ -36,6 +36,7 @@ export function consulMetricsSpecProvider(context: TutorialContext): TutorialSch
name: i18n.translate('home.tutorials.consulMetrics.nameTitle', {
defaultMessage: 'Consul metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.consulMetrics.shortDescription', {
defaultMessage: 'Fetch monitoring metrics from the Consul server.',
diff --git a/src/plugins/home/server/tutorials/coredns_logs/index.ts b/src/plugins/home/server/tutorials/coredns_logs/index.ts
index 4304fb7acb907..7fc8a2402d216 100644
--- a/src/plugins/home/server/tutorials/coredns_logs/index.ts
+++ b/src/plugins/home/server/tutorials/coredns_logs/index.ts
@@ -37,6 +37,7 @@ export function corednsLogsSpecProvider(context: TutorialContext): TutorialSchem
name: i18n.translate('home.tutorials.corednsLogs.nameTitle', {
defaultMessage: 'CoreDNS logs',
}),
+ moduleName,
category: TutorialsCategory.SECURITY_SOLUTION,
shortDescription: i18n.translate('home.tutorials.corednsLogs.shortDescription', {
defaultMessage: 'Collect the logs created by Coredns.',
diff --git a/src/plugins/home/server/tutorials/coredns_metrics/index.ts b/src/plugins/home/server/tutorials/coredns_metrics/index.ts
index 44bd0cb3999f6..c6589715ba9ce 100644
--- a/src/plugins/home/server/tutorials/coredns_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/coredns_metrics/index.ts
@@ -36,6 +36,7 @@ export function corednsMetricsSpecProvider(context: TutorialContext): TutorialSc
name: i18n.translate('home.tutorials.corednsMetrics.nameTitle', {
defaultMessage: 'CoreDNS metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.corednsMetrics.shortDescription', {
defaultMessage: 'Fetch monitoring metrics from the CoreDNS server.',
diff --git a/src/plugins/home/server/tutorials/couchbase_metrics/index.ts b/src/plugins/home/server/tutorials/couchbase_metrics/index.ts
index efd59029c9c50..370541c9324d8 100644
--- a/src/plugins/home/server/tutorials/couchbase_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/couchbase_metrics/index.ts
@@ -36,6 +36,7 @@ export function couchbaseMetricsSpecProvider(context: TutorialContext): Tutorial
name: i18n.translate('home.tutorials.couchbaseMetrics.nameTitle', {
defaultMessage: 'Couchbase metrics',
}),
+ moduleName,
isBeta: false,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.couchbaseMetrics.shortDescription', {
diff --git a/src/plugins/home/server/tutorials/couchdb_metrics/index.ts b/src/plugins/home/server/tutorials/couchdb_metrics/index.ts
index 1fbaa44817226..8d70fcf2a6cd7 100644
--- a/src/plugins/home/server/tutorials/couchdb_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/couchdb_metrics/index.ts
@@ -36,6 +36,7 @@ export function couchdbMetricsSpecProvider(context: TutorialContext): TutorialSc
name: i18n.translate('home.tutorials.couchdbMetrics.nameTitle', {
defaultMessage: 'CouchDB metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.couchdbMetrics.shortDescription', {
defaultMessage: 'Fetch monitoring metrics from the CouchdB server.',
diff --git a/src/plugins/home/server/tutorials/docker_metrics/index.ts b/src/plugins/home/server/tutorials/docker_metrics/index.ts
index 8c603697c4713..2e0c3ccb642dd 100644
--- a/src/plugins/home/server/tutorials/docker_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/docker_metrics/index.ts
@@ -36,6 +36,7 @@ export function dockerMetricsSpecProvider(context: TutorialContext): TutorialSch
name: i18n.translate('home.tutorials.dockerMetrics.nameTitle', {
defaultMessage: 'Docker metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.dockerMetrics.shortDescription', {
defaultMessage: 'Fetch metrics about your Docker containers.',
diff --git a/src/plugins/home/server/tutorials/dropwizard_metrics/index.ts b/src/plugins/home/server/tutorials/dropwizard_metrics/index.ts
index 008a7a9b3a697..d74db4b2ad958 100644
--- a/src/plugins/home/server/tutorials/dropwizard_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/dropwizard_metrics/index.ts
@@ -36,6 +36,7 @@ export function dropwizardMetricsSpecProvider(context: TutorialContext): Tutoria
name: i18n.translate('home.tutorials.dropwizardMetrics.nameTitle', {
defaultMessage: 'Dropwizard metrics',
}),
+ moduleName,
isBeta: false,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.dropwizardMetrics.shortDescription', {
diff --git a/src/plugins/home/server/tutorials/elasticsearch_logs/index.ts b/src/plugins/home/server/tutorials/elasticsearch_logs/index.ts
index 515b06ea82a5e..f6c280d29f67f 100644
--- a/src/plugins/home/server/tutorials/elasticsearch_logs/index.ts
+++ b/src/plugins/home/server/tutorials/elasticsearch_logs/index.ts
@@ -37,6 +37,7 @@ export function elasticsearchLogsSpecProvider(context: TutorialContext): Tutoria
name: i18n.translate('home.tutorials.elasticsearchLogs.nameTitle', {
defaultMessage: 'Elasticsearch logs',
}),
+ moduleName,
category: TutorialsCategory.LOGGING,
isBeta: true,
shortDescription: i18n.translate('home.tutorials.elasticsearchLogs.shortDescription', {
diff --git a/src/plugins/home/server/tutorials/elasticsearch_metrics/index.ts b/src/plugins/home/server/tutorials/elasticsearch_metrics/index.ts
index ea6dcf86d23e2..38713056e0640 100644
--- a/src/plugins/home/server/tutorials/elasticsearch_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/elasticsearch_metrics/index.ts
@@ -36,6 +36,7 @@ export function elasticsearchMetricsSpecProvider(context: TutorialContext): Tuto
name: i18n.translate('home.tutorials.elasticsearchMetrics.nameTitle', {
defaultMessage: 'Elasticsearch metrics',
}),
+ moduleName,
isBeta: false,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.elasticsearchMetrics.shortDescription', {
diff --git a/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts b/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts
index a9b9c33d61bdf..0cf032e6b90c1 100644
--- a/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts
+++ b/src/plugins/home/server/tutorials/envoyproxy_logs/index.ts
@@ -37,6 +37,7 @@ export function envoyproxyLogsSpecProvider(context: TutorialContext): TutorialSc
name: i18n.translate('home.tutorials.envoyproxyLogs.nameTitle', {
defaultMessage: 'Envoyproxy',
}),
+ moduleName,
category: TutorialsCategory.SECURITY_SOLUTION,
shortDescription: i18n.translate('home.tutorials.envoyproxyLogs.shortDescription', {
defaultMessage: 'Collect and parse logs received from the Envoy proxy.',
diff --git a/src/plugins/home/server/tutorials/envoyproxy_metrics/index.ts b/src/plugins/home/server/tutorials/envoyproxy_metrics/index.ts
index adc7a494200c1..9b453370fb802 100644
--- a/src/plugins/home/server/tutorials/envoyproxy_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/envoyproxy_metrics/index.ts
@@ -36,6 +36,7 @@ export function envoyproxyMetricsSpecProvider(context: TutorialContext): Tutoria
name: i18n.translate('home.tutorials.envoyproxyMetrics.nameTitle', {
defaultMessage: 'Envoy Proxy metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.envoyproxyMetrics.shortDescription', {
defaultMessage: 'Fetch monitoring metrics from Envoy Proxy.',
diff --git a/src/plugins/home/server/tutorials/etcd_metrics/index.ts b/src/plugins/home/server/tutorials/etcd_metrics/index.ts
index 2956473b6643b..48bdba5abb4b3 100644
--- a/src/plugins/home/server/tutorials/etcd_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/etcd_metrics/index.ts
@@ -36,6 +36,7 @@ export function etcdMetricsSpecProvider(context: TutorialContext): TutorialSchem
name: i18n.translate('home.tutorials.etcdMetrics.nameTitle', {
defaultMessage: 'Etcd metrics',
}),
+ moduleName,
isBeta: false,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.etcdMetrics.shortDescription', {
diff --git a/src/plugins/home/server/tutorials/golang_metrics/index.ts b/src/plugins/home/server/tutorials/golang_metrics/index.ts
index c53f8b2bba281..e5ecbb9eb583b 100644
--- a/src/plugins/home/server/tutorials/golang_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/golang_metrics/index.ts
@@ -36,6 +36,7 @@ export function golangMetricsSpecProvider(context: TutorialContext): TutorialSch
name: i18n.translate('home.tutorials.golangMetrics.nameTitle', {
defaultMessage: 'Golang metrics',
}),
+ moduleName,
isBeta: true,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.golangMetrics.shortDescription', {
diff --git a/src/plugins/home/server/tutorials/googlecloud_metrics/index.ts b/src/plugins/home/server/tutorials/googlecloud_metrics/index.ts
index 504ede04c12d8..42dc0720c10e0 100644
--- a/src/plugins/home/server/tutorials/googlecloud_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/googlecloud_metrics/index.ts
@@ -36,6 +36,7 @@ export function googlecloudMetricsSpecProvider(context: TutorialContext): Tutori
name: i18n.translate('home.tutorials.googlecloudMetrics.nameTitle', {
defaultMessage: 'Google Cloud metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.googlecloudMetrics.shortDescription', {
defaultMessage:
diff --git a/src/plugins/home/server/tutorials/haproxy_metrics/index.ts b/src/plugins/home/server/tutorials/haproxy_metrics/index.ts
index f06dfaa93063c..49e2ec4390db9 100644
--- a/src/plugins/home/server/tutorials/haproxy_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/haproxy_metrics/index.ts
@@ -36,6 +36,7 @@ export function haproxyMetricsSpecProvider(context: TutorialContext): TutorialSc
name: i18n.translate('home.tutorials.haproxyMetrics.nameTitle', {
defaultMessage: 'HAProxy metrics',
}),
+ moduleName,
isBeta: false,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.haproxyMetrics.shortDescription', {
diff --git a/src/plugins/home/server/tutorials/ibmmq_logs/index.ts b/src/plugins/home/server/tutorials/ibmmq_logs/index.ts
index 5739c03954def..8f67b88c3fcf2 100644
--- a/src/plugins/home/server/tutorials/ibmmq_logs/index.ts
+++ b/src/plugins/home/server/tutorials/ibmmq_logs/index.ts
@@ -37,6 +37,7 @@ export function ibmmqLogsSpecProvider(context: TutorialContext): TutorialSchema
name: i18n.translate('home.tutorials.ibmmqLogs.nameTitle', {
defaultMessage: 'IBM MQ logs',
}),
+ moduleName,
category: TutorialsCategory.LOGGING,
shortDescription: i18n.translate('home.tutorials.ibmmqLogs.shortDescription', {
defaultMessage: 'Collect IBM MQ logs with Filebeat.',
diff --git a/src/plugins/home/server/tutorials/ibmmq_metrics/index.ts b/src/plugins/home/server/tutorials/ibmmq_metrics/index.ts
index 4f20b2d0684fc..dc941233b0233 100644
--- a/src/plugins/home/server/tutorials/ibmmq_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/ibmmq_metrics/index.ts
@@ -36,6 +36,7 @@ export function ibmmqMetricsSpecProvider(context: TutorialContext): TutorialSche
name: i18n.translate('home.tutorials.ibmmqMetrics.nameTitle', {
defaultMessage: 'IBM MQ metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.ibmmqMetrics.shortDescription', {
defaultMessage: 'Fetch monitoring metrics from IBM MQ instances.',
diff --git a/src/plugins/home/server/tutorials/iis_logs/index.ts b/src/plugins/home/server/tutorials/iis_logs/index.ts
index fee8d036db757..12411fc792e64 100644
--- a/src/plugins/home/server/tutorials/iis_logs/index.ts
+++ b/src/plugins/home/server/tutorials/iis_logs/index.ts
@@ -37,6 +37,7 @@ export function iisLogsSpecProvider(context: TutorialContext): TutorialSchema {
name: i18n.translate('home.tutorials.iisLogs.nameTitle', {
defaultMessage: 'IIS logs',
}),
+ moduleName,
category: TutorialsCategory.LOGGING,
shortDescription: i18n.translate('home.tutorials.iisLogs.shortDescription', {
defaultMessage: 'Collect and parse access and error logs created by the IIS HTTP server.',
diff --git a/src/plugins/home/server/tutorials/iis_metrics/index.ts b/src/plugins/home/server/tutorials/iis_metrics/index.ts
index 46621677a67ce..d6dc5a2e33704 100644
--- a/src/plugins/home/server/tutorials/iis_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/iis_metrics/index.ts
@@ -36,6 +36,7 @@ export function iisMetricsSpecProvider(context: TutorialContext): TutorialSchema
name: i18n.translate('home.tutorials.iisMetrics.nameTitle', {
defaultMessage: 'IIS Metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.iisMetrics.shortDescription', {
defaultMessage: 'Collect IIS server related metrics.',
diff --git a/src/plugins/home/server/tutorials/iptables_logs/index.ts b/src/plugins/home/server/tutorials/iptables_logs/index.ts
index fd84894dae850..b3be133767447 100644
--- a/src/plugins/home/server/tutorials/iptables_logs/index.ts
+++ b/src/plugins/home/server/tutorials/iptables_logs/index.ts
@@ -37,6 +37,7 @@ export function iptablesLogsSpecProvider(context: TutorialContext): TutorialSche
name: i18n.translate('home.tutorials.iptablesLogs.nameTitle', {
defaultMessage: 'Iptables / Ubiquiti',
}),
+ moduleName,
category: TutorialsCategory.SECURITY_SOLUTION,
shortDescription: i18n.translate('home.tutorials.iptablesLogs.shortDescription', {
defaultMessage: 'Collect and parse iptables and ip6tables logs or from Ubiqiti firewalls.',
diff --git a/src/plugins/home/server/tutorials/kafka_logs/index.ts b/src/plugins/home/server/tutorials/kafka_logs/index.ts
index 746e65b71008c..aac172520829c 100644
--- a/src/plugins/home/server/tutorials/kafka_logs/index.ts
+++ b/src/plugins/home/server/tutorials/kafka_logs/index.ts
@@ -37,6 +37,7 @@ export function kafkaLogsSpecProvider(context: TutorialContext): TutorialSchema
name: i18n.translate('home.tutorials.kafkaLogs.nameTitle', {
defaultMessage: 'Kafka logs',
}),
+ moduleName,
category: TutorialsCategory.LOGGING,
shortDescription: i18n.translate('home.tutorials.kafkaLogs.shortDescription', {
defaultMessage: 'Collect and parse logs created by Kafka.',
diff --git a/src/plugins/home/server/tutorials/kafka_metrics/index.ts b/src/plugins/home/server/tutorials/kafka_metrics/index.ts
index 55860a3ab649a..1b0ce44db6550 100644
--- a/src/plugins/home/server/tutorials/kafka_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/kafka_metrics/index.ts
@@ -36,6 +36,7 @@ export function kafkaMetricsSpecProvider(context: TutorialContext): TutorialSche
name: i18n.translate('home.tutorials.kafkaMetrics.nameTitle', {
defaultMessage: 'Kafka metrics',
}),
+ moduleName,
isBeta: false,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.kafkaMetrics.shortDescription', {
diff --git a/src/plugins/home/server/tutorials/kibana_metrics/index.ts b/src/plugins/home/server/tutorials/kibana_metrics/index.ts
index fa966ac724a73..d595859959aca 100644
--- a/src/plugins/home/server/tutorials/kibana_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/kibana_metrics/index.ts
@@ -36,6 +36,7 @@ export function kibanaMetricsSpecProvider(context: TutorialContext): TutorialSch
name: i18n.translate('home.tutorials.kibanaMetrics.nameTitle', {
defaultMessage: 'Kibana metrics',
}),
+ moduleName,
isBeta: false,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.kibanaMetrics.shortDescription', {
diff --git a/src/plugins/home/server/tutorials/kubernetes_metrics/index.ts b/src/plugins/home/server/tutorials/kubernetes_metrics/index.ts
index bcea7f1221e1f..a4ce9cfab5f62 100644
--- a/src/plugins/home/server/tutorials/kubernetes_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/kubernetes_metrics/index.ts
@@ -36,6 +36,7 @@ export function kubernetesMetricsSpecProvider(context: TutorialContext): Tutoria
name: i18n.translate('home.tutorials.kubernetesMetrics.nameTitle', {
defaultMessage: 'Kubernetes metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.kubernetesMetrics.shortDescription', {
defaultMessage: 'Fetch metrics from your Kubernetes installation.',
diff --git a/src/plugins/home/server/tutorials/logstash_logs/index.ts b/src/plugins/home/server/tutorials/logstash_logs/index.ts
index 69e498ac59459..32982cd1055a4 100644
--- a/src/plugins/home/server/tutorials/logstash_logs/index.ts
+++ b/src/plugins/home/server/tutorials/logstash_logs/index.ts
@@ -37,6 +37,7 @@ export function logstashLogsSpecProvider(context: TutorialContext): TutorialSche
name: i18n.translate('home.tutorials.logstashLogs.nameTitle', {
defaultMessage: 'Logstash logs',
}),
+ moduleName,
category: TutorialsCategory.LOGGING,
shortDescription: i18n.translate('home.tutorials.logstashLogs.shortDescription', {
defaultMessage: 'Collect and parse debug and slow logs created by Logstash itself.',
diff --git a/src/plugins/home/server/tutorials/logstash_metrics/index.ts b/src/plugins/home/server/tutorials/logstash_metrics/index.ts
index 383273a8c365c..11272b7ceef6b 100644
--- a/src/plugins/home/server/tutorials/logstash_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/logstash_metrics/index.ts
@@ -36,6 +36,7 @@ export function logstashMetricsSpecProvider(context: TutorialContext): TutorialS
name: i18n.translate('home.tutorials.logstashMetrics.nameTitle', {
defaultMessage: 'Logstash metrics',
}),
+ moduleName,
isBeta: false,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.logstashMetrics.shortDescription', {
diff --git a/src/plugins/home/server/tutorials/memcached_metrics/index.ts b/src/plugins/home/server/tutorials/memcached_metrics/index.ts
index 94451556ad34c..c724b790f84a6 100644
--- a/src/plugins/home/server/tutorials/memcached_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/memcached_metrics/index.ts
@@ -36,6 +36,7 @@ export function memcachedMetricsSpecProvider(context: TutorialContext): Tutorial
name: i18n.translate('home.tutorials.memcachedMetrics.nameTitle', {
defaultMessage: 'Memcached metrics',
}),
+ moduleName,
isBeta: false,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.memcachedMetrics.shortDescription', {
diff --git a/src/plugins/home/server/tutorials/mongodb_metrics/index.ts b/src/plugins/home/server/tutorials/mongodb_metrics/index.ts
index f02695e207dd3..2f39a048f2f15 100644
--- a/src/plugins/home/server/tutorials/mongodb_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/mongodb_metrics/index.ts
@@ -36,6 +36,7 @@ export function mongodbMetricsSpecProvider(context: TutorialContext): TutorialSc
name: i18n.translate('home.tutorials.mongodbMetrics.nameTitle', {
defaultMessage: 'MongoDB metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.mongodbMetrics.shortDescription', {
defaultMessage: 'Fetch internal metrics from MongoDB.',
diff --git a/src/plugins/home/server/tutorials/mssql_metrics/index.ts b/src/plugins/home/server/tutorials/mssql_metrics/index.ts
index 4b418587f78b2..1a1f047a12848 100644
--- a/src/plugins/home/server/tutorials/mssql_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/mssql_metrics/index.ts
@@ -36,6 +36,7 @@ export function mssqlMetricsSpecProvider(context: TutorialContext): TutorialSche
name: i18n.translate('home.tutorials.mssqlMetrics.nameTitle', {
defaultMessage: 'Microsoft SQL Server Metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.mssqlMetrics.shortDescription', {
defaultMessage: 'Fetch monitoring metrics from a Microsoft SQL Server instance',
diff --git a/src/plugins/home/server/tutorials/munin_metrics/index.ts b/src/plugins/home/server/tutorials/munin_metrics/index.ts
index 3dbb34cb22031..8434d916daa1f 100644
--- a/src/plugins/home/server/tutorials/munin_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/munin_metrics/index.ts
@@ -36,6 +36,7 @@ export function muninMetricsSpecProvider(context: TutorialContext): TutorialSche
name: i18n.translate('home.tutorials.muninMetrics.nameTitle', {
defaultMessage: 'Munin metrics',
}),
+ moduleName,
euiIconType: '/plugins/home/assets/logos/munin.svg',
isBeta: true,
category: TutorialsCategory.METRICS,
diff --git a/src/plugins/home/server/tutorials/mysql_logs/index.ts b/src/plugins/home/server/tutorials/mysql_logs/index.ts
index 178a371f9212e..37bbf409b91c5 100644
--- a/src/plugins/home/server/tutorials/mysql_logs/index.ts
+++ b/src/plugins/home/server/tutorials/mysql_logs/index.ts
@@ -37,6 +37,7 @@ export function mysqlLogsSpecProvider(context: TutorialContext): TutorialSchema
name: i18n.translate('home.tutorials.mysqlLogs.nameTitle', {
defaultMessage: 'MySQL logs',
}),
+ moduleName,
category: TutorialsCategory.LOGGING,
shortDescription: i18n.translate('home.tutorials.mysqlLogs.shortDescription', {
defaultMessage: 'Collect and parse error and slow logs created by MySQL.',
diff --git a/src/plugins/home/server/tutorials/mysql_metrics/index.ts b/src/plugins/home/server/tutorials/mysql_metrics/index.ts
index 1148caeb441f8..89f5edf22a7b6 100644
--- a/src/plugins/home/server/tutorials/mysql_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/mysql_metrics/index.ts
@@ -36,6 +36,7 @@ export function mysqlMetricsSpecProvider(context: TutorialContext): TutorialSche
name: i18n.translate('home.tutorials.mysqlMetrics.nameTitle', {
defaultMessage: 'MySQL metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.mysqlMetrics.shortDescription', {
defaultMessage: 'Fetch internal metrics from MySQL.',
diff --git a/src/plugins/home/server/tutorials/nats_logs/index.ts b/src/plugins/home/server/tutorials/nats_logs/index.ts
index 17c37755b6bc3..f00ddd6ca8879 100644
--- a/src/plugins/home/server/tutorials/nats_logs/index.ts
+++ b/src/plugins/home/server/tutorials/nats_logs/index.ts
@@ -37,6 +37,7 @@ export function natsLogsSpecProvider(context: TutorialContext): TutorialSchema {
name: i18n.translate('home.tutorials.natsLogs.nameTitle', {
defaultMessage: 'NATS logs',
}),
+ moduleName,
category: TutorialsCategory.LOGGING,
isBeta: true,
shortDescription: i18n.translate('home.tutorials.natsLogs.shortDescription', {
diff --git a/src/plugins/home/server/tutorials/nats_metrics/index.ts b/src/plugins/home/server/tutorials/nats_metrics/index.ts
index bce08e85c6977..cda011297d2c6 100644
--- a/src/plugins/home/server/tutorials/nats_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/nats_metrics/index.ts
@@ -36,6 +36,7 @@ export function natsMetricsSpecProvider(context: TutorialContext): TutorialSchem
name: i18n.translate('home.tutorials.natsMetrics.nameTitle', {
defaultMessage: 'NATS metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.natsMetrics.shortDescription', {
defaultMessage: 'Fetch monitoring metrics from the Nats server.',
diff --git a/src/plugins/home/server/tutorials/netflow/index.ts b/src/plugins/home/server/tutorials/netflow/index.ts
index ec0aa8953b146..5be30bbb152b7 100644
--- a/src/plugins/home/server/tutorials/netflow/index.ts
+++ b/src/plugins/home/server/tutorials/netflow/index.ts
@@ -25,9 +25,11 @@ import { createElasticCloudInstructions } from './elastic_cloud';
import { createOnPremElasticCloudInstructions } from './on_prem_elastic_cloud';
export function netflowSpecProvider() {
+ const moduleName = 'netflow';
return {
id: 'netflow',
name: 'Netflow',
+ moduleName,
category: TutorialsCategory.SECURITY_SOLUTION,
shortDescription: i18n.translate('home.tutorials.netflow.tutorialShortDescription', {
defaultMessage: 'Collect Netflow records sent by a Netflow exporter.',
diff --git a/src/plugins/home/server/tutorials/nginx_logs/index.ts b/src/plugins/home/server/tutorials/nginx_logs/index.ts
index 37d0cc106bfe5..f357e77fc25ca 100644
--- a/src/plugins/home/server/tutorials/nginx_logs/index.ts
+++ b/src/plugins/home/server/tutorials/nginx_logs/index.ts
@@ -37,6 +37,7 @@ export function nginxLogsSpecProvider(context: TutorialContext): TutorialSchema
name: i18n.translate('home.tutorials.nginxLogs.nameTitle', {
defaultMessage: 'Nginx logs',
}),
+ moduleName,
category: TutorialsCategory.LOGGING,
shortDescription: i18n.translate('home.tutorials.nginxLogs.shortDescription', {
defaultMessage: 'Collect and parse access and error logs created by the Nginx HTTP server.',
diff --git a/src/plugins/home/server/tutorials/nginx_metrics/index.ts b/src/plugins/home/server/tutorials/nginx_metrics/index.ts
index 8671f7218ffc8..09031883cef1c 100644
--- a/src/plugins/home/server/tutorials/nginx_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/nginx_metrics/index.ts
@@ -36,6 +36,7 @@ export function nginxMetricsSpecProvider(context: TutorialContext): TutorialSche
name: i18n.translate('home.tutorials.nginxMetrics.nameTitle', {
defaultMessage: 'Nginx metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.nginxMetrics.shortDescription', {
defaultMessage: 'Fetch internal metrics from the Nginx HTTP server.',
diff --git a/src/plugins/home/server/tutorials/openmetrics_metrics/index.ts b/src/plugins/home/server/tutorials/openmetrics_metrics/index.ts
index eb539e15c1bcd..197821f24dddb 100644
--- a/src/plugins/home/server/tutorials/openmetrics_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/openmetrics_metrics/index.ts
@@ -36,6 +36,7 @@ export function openmetricsMetricsSpecProvider(context: TutorialContext): Tutori
name: i18n.translate('home.tutorials.openmetricsMetrics.nameTitle', {
defaultMessage: 'OpenMetrics metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.openmetricsMetrics.shortDescription', {
defaultMessage: 'Fetch metrics from an endpoint that serves metrics in OpenMetrics format.',
diff --git a/src/plugins/home/server/tutorials/oracle_metrics/index.ts b/src/plugins/home/server/tutorials/oracle_metrics/index.ts
index 3144b0a21aab5..d2ddd19b930a2 100644
--- a/src/plugins/home/server/tutorials/oracle_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/oracle_metrics/index.ts
@@ -36,6 +36,7 @@ export function oracleMetricsSpecProvider(context: TutorialContext): TutorialSch
name: i18n.translate('home.tutorials.oracleMetrics.nameTitle', {
defaultMessage: 'oracle metrics',
}),
+ moduleName,
isBeta: false,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.oracleMetrics.shortDescription', {
diff --git a/src/plugins/home/server/tutorials/osquery_logs/index.ts b/src/plugins/home/server/tutorials/osquery_logs/index.ts
index 8781d6201a771..c4869a889a085 100644
--- a/src/plugins/home/server/tutorials/osquery_logs/index.ts
+++ b/src/plugins/home/server/tutorials/osquery_logs/index.ts
@@ -37,6 +37,7 @@ export function osqueryLogsSpecProvider(context: TutorialContext): TutorialSchem
name: i18n.translate('home.tutorials.osqueryLogs.nameTitle', {
defaultMessage: 'Osquery logs',
}),
+ moduleName,
category: TutorialsCategory.SECURITY_SOLUTION,
shortDescription: i18n.translate('home.tutorials.osqueryLogs.shortDescription', {
defaultMessage: 'Collect the result logs created by osqueryd.',
diff --git a/src/plugins/home/server/tutorials/php_fpm_metrics/index.ts b/src/plugins/home/server/tutorials/php_fpm_metrics/index.ts
index 975b549c9520b..470cfed2176fd 100644
--- a/src/plugins/home/server/tutorials/php_fpm_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/php_fpm_metrics/index.ts
@@ -36,6 +36,7 @@ export function phpfpmMetricsSpecProvider(context: TutorialContext): TutorialSch
name: i18n.translate('home.tutorials.phpFpmMetrics.nameTitle', {
defaultMessage: 'PHP-FPM metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
isBeta: false,
shortDescription: i18n.translate('home.tutorials.phpFpmMetrics.shortDescription', {
diff --git a/src/plugins/home/server/tutorials/postgresql_logs/index.ts b/src/plugins/home/server/tutorials/postgresql_logs/index.ts
index 0c28061985819..e158dedcb03e0 100644
--- a/src/plugins/home/server/tutorials/postgresql_logs/index.ts
+++ b/src/plugins/home/server/tutorials/postgresql_logs/index.ts
@@ -37,6 +37,7 @@ export function postgresqlLogsSpecProvider(context: TutorialContext): TutorialSc
name: i18n.translate('home.tutorials.postgresqlLogs.nameTitle', {
defaultMessage: 'PostgreSQL logs',
}),
+ moduleName,
category: TutorialsCategory.LOGGING,
shortDescription: i18n.translate('home.tutorials.postgresqlLogs.shortDescription', {
defaultMessage: 'Collect and parse error and slow logs created by PostgreSQL.',
diff --git a/src/plugins/home/server/tutorials/postgresql_metrics/index.ts b/src/plugins/home/server/tutorials/postgresql_metrics/index.ts
index f9bb9d249e755..1add49c10c2a7 100644
--- a/src/plugins/home/server/tutorials/postgresql_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/postgresql_metrics/index.ts
@@ -36,6 +36,7 @@ export function postgresqlMetricsSpecProvider(context: TutorialContext): Tutoria
name: i18n.translate('home.tutorials.postgresqlMetrics.nameTitle', {
defaultMessage: 'PostgreSQL metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
isBeta: false,
shortDescription: i18n.translate('home.tutorials.postgresqlMetrics.shortDescription', {
diff --git a/src/plugins/home/server/tutorials/prometheus_metrics/index.ts b/src/plugins/home/server/tutorials/prometheus_metrics/index.ts
index 06e8a138049d5..900c5da7cdbe3 100644
--- a/src/plugins/home/server/tutorials/prometheus_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/prometheus_metrics/index.ts
@@ -36,6 +36,7 @@ export function prometheusMetricsSpecProvider(context: TutorialContext): Tutoria
name: i18n.translate('home.tutorials.prometheusMetrics.nameTitle', {
defaultMessage: 'Prometheus metrics',
}),
+ moduleName,
isBeta: false,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.prometheusMetrics.shortDescription', {
diff --git a/src/plugins/home/server/tutorials/rabbitmq_metrics/index.ts b/src/plugins/home/server/tutorials/rabbitmq_metrics/index.ts
index a646068e4ff34..df0aa57d9feac 100644
--- a/src/plugins/home/server/tutorials/rabbitmq_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/rabbitmq_metrics/index.ts
@@ -36,6 +36,7 @@ export function rabbitmqMetricsSpecProvider(context: TutorialContext): TutorialS
name: i18n.translate('home.tutorials.rabbitmqMetrics.nameTitle', {
defaultMessage: 'RabbitMQ metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.rabbitmqMetrics.shortDescription', {
defaultMessage: 'Fetch internal metrics from the RabbitMQ server.',
diff --git a/src/plugins/home/server/tutorials/redis_logs/index.ts b/src/plugins/home/server/tutorials/redis_logs/index.ts
index e017fae0499a3..785118b9e5d09 100644
--- a/src/plugins/home/server/tutorials/redis_logs/index.ts
+++ b/src/plugins/home/server/tutorials/redis_logs/index.ts
@@ -37,6 +37,7 @@ export function redisLogsSpecProvider(context: TutorialContext): TutorialSchema
name: i18n.translate('home.tutorials.redisLogs.nameTitle', {
defaultMessage: 'Redis logs',
}),
+ moduleName,
category: TutorialsCategory.LOGGING,
shortDescription: i18n.translate('home.tutorials.redisLogs.shortDescription', {
defaultMessage: 'Collect and parse error and slow logs created by Redis.',
diff --git a/src/plugins/home/server/tutorials/redis_metrics/index.ts b/src/plugins/home/server/tutorials/redis_metrics/index.ts
index bcc4d9bb0b67b..11d05029844b2 100644
--- a/src/plugins/home/server/tutorials/redis_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/redis_metrics/index.ts
@@ -36,6 +36,7 @@ export function redisMetricsSpecProvider(context: TutorialContext): TutorialSche
name: i18n.translate('home.tutorials.redisMetrics.nameTitle', {
defaultMessage: 'Redis metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.redisMetrics.shortDescription', {
defaultMessage: 'Fetch internal metrics from Redis.',
diff --git a/src/plugins/home/server/tutorials/redisenterprise_metrics/index.ts b/src/plugins/home/server/tutorials/redisenterprise_metrics/index.ts
index ffbb5ab75da87..0bc7769f950ed 100644
--- a/src/plugins/home/server/tutorials/redisenterprise_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/redisenterprise_metrics/index.ts
@@ -36,6 +36,7 @@ export function redisenterpriseMetricsSpecProvider(context: TutorialContext): Tu
name: i18n.translate('home.tutorials.redisenterpriseMetrics.nameTitle', {
defaultMessage: 'Redis Enterprise metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.redisenterpriseMetrics.shortDescription', {
defaultMessage: 'Fetch monitoring metrics from Redis Enterprise Server.',
diff --git a/src/plugins/home/server/tutorials/stan_metrics/index.ts b/src/plugins/home/server/tutorials/stan_metrics/index.ts
index 616bc7450249e..b1ad3e9c1404a 100644
--- a/src/plugins/home/server/tutorials/stan_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/stan_metrics/index.ts
@@ -36,6 +36,7 @@ export function stanMetricsSpecProvider(context: TutorialContext): TutorialSchem
name: i18n.translate('home.tutorials.stanMetrics.nameTitle', {
defaultMessage: 'STAN metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.stanMetrics.shortDescription', {
defaultMessage: 'Fetch monitoring metrics from the STAN server.',
diff --git a/src/plugins/home/server/tutorials/statsd_metrics/index.ts b/src/plugins/home/server/tutorials/statsd_metrics/index.ts
index 1dc297e78c791..9e9d7d6fd3e23 100644
--- a/src/plugins/home/server/tutorials/statsd_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/statsd_metrics/index.ts
@@ -33,6 +33,7 @@ export function statsdMetricsSpecProvider(context: TutorialContext): TutorialSch
name: i18n.translate('home.tutorials.statsdMetrics.nameTitle', {
defaultMessage: 'Statsd metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.statsdMetrics.shortDescription', {
defaultMessage: 'Fetch monitoring metrics from statsd.',
diff --git a/src/plugins/home/server/tutorials/suricata_logs/index.ts b/src/plugins/home/server/tutorials/suricata_logs/index.ts
index 6bcfc1d43a250..eec81b9496647 100644
--- a/src/plugins/home/server/tutorials/suricata_logs/index.ts
+++ b/src/plugins/home/server/tutorials/suricata_logs/index.ts
@@ -37,6 +37,7 @@ export function suricataLogsSpecProvider(context: TutorialContext): TutorialSche
name: i18n.translate('home.tutorials.suricataLogs.nameTitle', {
defaultMessage: 'Suricata logs',
}),
+ moduleName,
category: TutorialsCategory.SECURITY_SOLUTION,
shortDescription: i18n.translate('home.tutorials.suricataLogs.shortDescription', {
defaultMessage: 'Collect the result logs created by Suricata IDS/IPS/NSM.',
diff --git a/src/plugins/home/server/tutorials/system_logs/index.ts b/src/plugins/home/server/tutorials/system_logs/index.ts
index 9bad70699a6ed..f39df25461a5f 100644
--- a/src/plugins/home/server/tutorials/system_logs/index.ts
+++ b/src/plugins/home/server/tutorials/system_logs/index.ts
@@ -37,6 +37,7 @@ export function systemLogsSpecProvider(context: TutorialContext): TutorialSchema
name: i18n.translate('home.tutorials.systemLogs.nameTitle', {
defaultMessage: 'System logs',
}),
+ moduleName,
category: TutorialsCategory.LOGGING,
shortDescription: i18n.translate('home.tutorials.systemLogs.shortDescription', {
defaultMessage: 'Collect and parse logs written by the local Syslog server.',
diff --git a/src/plugins/home/server/tutorials/system_metrics/index.ts b/src/plugins/home/server/tutorials/system_metrics/index.ts
index ef1a84ecdbf10..6bdaaa34a9b2c 100644
--- a/src/plugins/home/server/tutorials/system_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/system_metrics/index.ts
@@ -36,6 +36,7 @@ export function systemMetricsSpecProvider(context: TutorialContext): TutorialSch
name: i18n.translate('home.tutorials.systemMetrics.nameTitle', {
defaultMessage: 'System metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.systemMetrics.shortDescription', {
defaultMessage: 'Collect CPU, memory, network, and disk statistics from the host.',
diff --git a/src/plugins/home/server/tutorials/traefik_logs/index.ts b/src/plugins/home/server/tutorials/traefik_logs/index.ts
index 1876edd6c0bf7..0a84dcb081883 100644
--- a/src/plugins/home/server/tutorials/traefik_logs/index.ts
+++ b/src/plugins/home/server/tutorials/traefik_logs/index.ts
@@ -37,6 +37,7 @@ export function traefikLogsSpecProvider(context: TutorialContext): TutorialSchem
name: i18n.translate('home.tutorials.traefikLogs.nameTitle', {
defaultMessage: 'Traefik logs',
}),
+ moduleName,
category: TutorialsCategory.LOGGING,
shortDescription: i18n.translate('home.tutorials.traefikLogs.shortDescription', {
defaultMessage: 'Collect and parse access logs created by the Traefik Proxy.',
diff --git a/src/plugins/home/server/tutorials/traefik_metrics/index.ts b/src/plugins/home/server/tutorials/traefik_metrics/index.ts
index a97ee3ab9758a..4048719239a10 100644
--- a/src/plugins/home/server/tutorials/traefik_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/traefik_metrics/index.ts
@@ -33,6 +33,7 @@ export function traefikMetricsSpecProvider(context: TutorialContext): TutorialSc
name: i18n.translate('home.tutorials.traefikMetrics.nameTitle', {
defaultMessage: 'Traefik metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.traefikMetrics.shortDescription', {
defaultMessage: 'Fetch monitoring metrics from Traefik.',
diff --git a/src/plugins/home/server/tutorials/uptime_monitors/index.ts b/src/plugins/home/server/tutorials/uptime_monitors/index.ts
index fa854a1c23505..7366583e59778 100644
--- a/src/plugins/home/server/tutorials/uptime_monitors/index.ts
+++ b/src/plugins/home/server/tutorials/uptime_monitors/index.ts
@@ -30,11 +30,13 @@ import {
} from '../../services/tutorials/lib/tutorials_registry_types';
export function uptimeMonitorsSpecProvider(context: TutorialContext): TutorialSchema {
+ const moduleName = 'uptime';
return {
id: 'uptimeMonitors',
name: i18n.translate('home.tutorials.uptimeMonitors.nameTitle', {
defaultMessage: 'Uptime Monitors',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.uptimeMonitors.shortDescription', {
defaultMessage: 'Monitor services for their availability',
diff --git a/src/plugins/home/server/tutorials/uwsgi_metrics/index.ts b/src/plugins/home/server/tutorials/uwsgi_metrics/index.ts
index bbe4ea78ee87c..f6398be3550fd 100644
--- a/src/plugins/home/server/tutorials/uwsgi_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/uwsgi_metrics/index.ts
@@ -36,6 +36,7 @@ export function uwsgiMetricsSpecProvider(context: TutorialContext): TutorialSche
name: i18n.translate('home.tutorials.uwsgiMetrics.nameTitle', {
defaultMessage: 'uWSGI metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.uwsgiMetrics.shortDescription', {
defaultMessage: 'Fetch internal metrics from the uWSGI server.',
diff --git a/src/plugins/home/server/tutorials/vsphere_metrics/index.ts b/src/plugins/home/server/tutorials/vsphere_metrics/index.ts
index 4450ab3040750..5e1191ffdf8ce 100644
--- a/src/plugins/home/server/tutorials/vsphere_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/vsphere_metrics/index.ts
@@ -36,6 +36,7 @@ export function vSphereMetricsSpecProvider(context: TutorialContext): TutorialSc
name: i18n.translate('home.tutorials.vsphereMetrics.nameTitle', {
defaultMessage: 'vSphere metrics',
}),
+ moduleName,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.vsphereMetrics.shortDescription', {
defaultMessage: 'Fetch internal metrics from vSphere.',
diff --git a/src/plugins/home/server/tutorials/windows_event_logs/index.ts b/src/plugins/home/server/tutorials/windows_event_logs/index.ts
index c2ea9ff3015e4..80f7a58ae14be 100644
--- a/src/plugins/home/server/tutorials/windows_event_logs/index.ts
+++ b/src/plugins/home/server/tutorials/windows_event_logs/index.ts
@@ -30,11 +30,13 @@ import {
} from '../../services/tutorials/lib/tutorials_registry_types';
export function windowsEventLogsSpecProvider(context: TutorialContext): TutorialSchema {
+ const moduleName = 'windows';
return {
id: 'windowsEventLogs',
name: i18n.translate('home.tutorials.windowsEventLogs.nameTitle', {
defaultMessage: 'Windows Event Log',
}),
+ moduleName,
isBeta: false,
category: TutorialsCategory.SECURITY_SOLUTION,
shortDescription: i18n.translate('home.tutorials.windowsEventLogs.shortDescription', {
diff --git a/src/plugins/home/server/tutorials/windows_metrics/index.ts b/src/plugins/home/server/tutorials/windows_metrics/index.ts
index 5333a7b1badf6..18cdcdc985e54 100644
--- a/src/plugins/home/server/tutorials/windows_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/windows_metrics/index.ts
@@ -36,6 +36,7 @@ export function windowsMetricsSpecProvider(context: TutorialContext): TutorialSc
name: i18n.translate('home.tutorials.windowsMetrics.nameTitle', {
defaultMessage: 'Windows metrics',
}),
+ moduleName,
isBeta: false,
category: TutorialsCategory.METRICS,
shortDescription: i18n.translate('home.tutorials.windowsMetrics.shortDescription', {
diff --git a/src/plugins/home/server/tutorials/zeek_logs/index.ts b/src/plugins/home/server/tutorials/zeek_logs/index.ts
index c273a93b1b0d5..e39dcd3409490 100644
--- a/src/plugins/home/server/tutorials/zeek_logs/index.ts
+++ b/src/plugins/home/server/tutorials/zeek_logs/index.ts
@@ -37,6 +37,7 @@ export function zeekLogsSpecProvider(context: TutorialContext): TutorialSchema {
name: i18n.translate('home.tutorials.zeekLogs.nameTitle', {
defaultMessage: 'Zeek logs',
}),
+ moduleName,
category: TutorialsCategory.SECURITY_SOLUTION,
shortDescription: i18n.translate('home.tutorials.zeekLogs.shortDescription', {
defaultMessage: 'Collect the logs created by Zeek/Bro.',
diff --git a/src/plugins/home/server/tutorials/zookeeper_metrics/index.ts b/src/plugins/home/server/tutorials/zookeeper_metrics/index.ts
index ae146d192432b..a39540b7399e5 100644
--- a/src/plugins/home/server/tutorials/zookeeper_metrics/index.ts
+++ b/src/plugins/home/server/tutorials/zookeeper_metrics/index.ts
@@ -36,6 +36,7 @@ export function zookeeperMetricsSpecProvider(context: TutorialContext): Tutorial
name: i18n.translate('home.tutorials.zookeeperMetrics.nameTitle', {
defaultMessage: 'Zookeeper metrics',
}),
+ moduleName,
euiIconType: '/plugins/home/assets/logos/zookeeper.svg',
isBeta: false,
category: TutorialsCategory.METRICS,
diff --git a/src/plugins/index_pattern_management/kibana.json b/src/plugins/index_pattern_management/kibana.json
index 23adef2626a72..d0ad6a96065c3 100644
--- a/src/plugins/index_pattern_management/kibana.json
+++ b/src/plugins/index_pattern_management/kibana.json
@@ -3,5 +3,6 @@
"version": "kibana",
"server": true,
"ui": true,
- "requiredPlugins": ["management", "data", "kibanaLegacy"]
+ "requiredPlugins": ["management", "data", "kibanaLegacy"],
+ "requiredBundles": ["kibanaReact", "kibanaUtils"]
}
diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap
index 70200e03c0dbe..6d79515c172fe 100644
--- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap
+++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/__snapshots__/create_index_pattern_wizard.test.tsx.snap
@@ -5,6 +5,7 @@ exports[`CreateIndexPatternWizard defaults to the loading state 1`] = `
@@ -52,6 +53,7 @@ exports[`CreateIndexPatternWizard renders index pattern step when there are indi
@@ -66,6 +68,7 @@ exports[`CreateIndexPatternWizard renders the empty state when there are no indi
/>
@@ -107,6 +110,7 @@ exports[`CreateIndexPatternWizard renders time field step when step is set to 2
@@ -148,6 +152,7 @@ exports[`CreateIndexPatternWizard renders when there are no indices but there ar
@@ -162,6 +167,7 @@ exports[`CreateIndexPatternWizard shows system indices even if there are no othe
/>
diff --git a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/__snapshots__/header.test.tsx.snap b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/__snapshots__/header.test.tsx.snap
index 6a2fd1000e6b4..0c89fb494b6ac 100644
--- a/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/__snapshots__/header.test.tsx.snap
+++ b/src/plugins/index_pattern_management/public/components/create_index_pattern_wizard/components/header/__snapshots__/header.test.tsx.snap
@@ -49,7 +49,7 @@ exports[`Header should render a different name, prompt, and beta tag if provided
>
multiple
- data souces,
+ data sources,
multiple
- data souces,
+ data sources,
multiple
- data souces,
+ data sources,
multiple,
single: filebeat-4-3-22 ,
diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx
index ab5a253a98e29..e43ee2e55eeca 100644
--- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx
+++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx
@@ -21,7 +21,7 @@ import React, { ReactElement } from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import { Table, TableProps, TableState } from './table';
-import { EuiTableFieldDataColumnType, keyCodes } from '@elastic/eui';
+import { EuiTableFieldDataColumnType, keys } from '@elastic/eui';
import { IIndexPattern } from 'src/plugins/data/public';
import { SourceFiltersTableFilter } from '../../types';
@@ -250,7 +250,7 @@ describe('Table', () => {
);
// Press the enter key
- filterNameTableCell.find('EuiFieldText').simulate('keydown', { keyCode: keyCodes.ENTER });
+ filterNameTableCell.find('EuiFieldText').simulate('keydown', { key: keys.ENTER });
expect(saveFilter).toBeCalled();
// It should reset
@@ -289,7 +289,7 @@ describe('Table', () => {
);
// Press the ESCAPE key
- filterNameTableCell.find('EuiFieldText').simulate('keydown', { keyCode: keyCodes.ESCAPE });
+ filterNameTableCell.find('EuiFieldText').simulate('keydown', { key: keys.ESCAPE });
expect(saveFilter).not.toBeCalled();
// It should reset
diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx
index 04998d9f7dafe..f73d756f28116 100644
--- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx
+++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx
@@ -20,7 +20,7 @@
import React, { Component } from 'react';
import {
- keyCodes,
+ keys,
EuiBasicTableColumn,
EuiInMemoryTable,
EuiFieldText,
@@ -111,15 +111,15 @@ export class Table extends Component {
onEditingFilterChange = (e: React.ChangeEvent) =>
this.setState({ editingFilterValue: e.target.value });
- onEditFieldKeyDown = ({ keyCode }: React.KeyboardEvent) => {
- if (keyCodes.ENTER === keyCode && this.state.editingFilterId && this.state.editingFilterValue) {
+ onEditFieldKeyDown = ({ key }: React.KeyboardEvent) => {
+ if (keys.ENTER === key && this.state.editingFilterId && this.state.editingFilterValue) {
this.props.saveFilter({
clientId: this.state.editingFilterId,
value: this.state.editingFilterValue,
});
this.stopEditingFilter();
}
- if (keyCodes.ESCAPE === keyCode) {
+ if (keys.ESCAPE === key) {
this.stopEditingFilter();
}
};
diff --git a/src/plugins/input_control_vis/kibana.json b/src/plugins/input_control_vis/kibana.json
index 4a4ec328c1352..6928eb19d02e1 100644
--- a/src/plugins/input_control_vis/kibana.json
+++ b/src/plugins/input_control_vis/kibana.json
@@ -4,5 +4,6 @@
"kibanaVersion": "kibana",
"server": true,
"ui": true,
- "requiredPlugins": ["data", "expressions", "visualizations"]
+ "requiredPlugins": ["data", "expressions", "visualizations"],
+ "requiredBundles": ["kibanaReact"]
}
diff --git a/src/plugins/inspector/kibana.json b/src/plugins/inspector/kibana.json
index 99a38d2928df6..90e5d60250728 100644
--- a/src/plugins/inspector/kibana.json
+++ b/src/plugins/inspector/kibana.json
@@ -3,5 +3,6 @@
"version": "kibana",
"server": false,
"ui": true,
- "extraPublicDirs": ["common", "common/adapters/request"]
+ "extraPublicDirs": ["common", "common/adapters/request"],
+ "requiredBundles": ["kibanaReact"]
}
diff --git a/src/plugins/inspector/public/views/data/components/__snapshots__/data_view.test.tsx.snap b/src/plugins/inspector/public/views/data/components/__snapshots__/data_view.test.tsx.snap
index adea7831d6b80..2632afff2f63b 100644
--- a/src/plugins/inspector/public/views/data/components/__snapshots__/data_view.test.tsx.snap
+++ b/src/plugins/inspector/public/views/data/components/__snapshots__/data_view.test.tsx.snap
@@ -269,7 +269,9 @@ exports[`Inspector Data View component should render empty state 1`] = `
-
+
diff --git a/src/plugins/kibana_legacy/public/utils/kbn_accessible_click.js b/src/plugins/kibana_legacy/public/utils/kbn_accessible_click.js
index ba1363ef06285..2dbf4002da748 100644
--- a/src/plugins/kibana_legacy/public/utils/kbn_accessible_click.js
+++ b/src/plugins/kibana_legacy/public/utils/kbn_accessible_click.js
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { accessibleClickKeys, keyCodes } from '@elastic/eui';
+import { accessibleClickKeys, keys } from '@elastic/eui';
export function KbnAccessibleClickProvider() {
return {
@@ -24,7 +24,7 @@ export function KbnAccessibleClickProvider() {
controller: ($element) => {
$element.on('keydown', (e) => {
// Prevent a scroll from occurring if the user has hit space.
- if (e.keyCode === keyCodes.SPACE) {
+ if (e.key === keys.SPACE) {
e.preventDefault();
}
});
@@ -60,7 +60,7 @@ export function KbnAccessibleClickProvider() {
element.on('keyup', (e) => {
// Support keyboard accessibility by emulating mouse click on ENTER or SPACE keypress.
- if (accessibleClickKeys[e.keyCode]) {
+ if (accessibleClickKeys[e.key]) {
// Delegate to the click handler on the element (assumed to be ng-click).
element.click();
}
diff --git a/src/plugins/kibana_react/kibana.json b/src/plugins/kibana_react/kibana.json
index 0add1bee84ae0..a507fe457b633 100644
--- a/src/plugins/kibana_react/kibana.json
+++ b/src/plugins/kibana_react/kibana.json
@@ -1,5 +1,6 @@
{
"id": "kibanaReact",
"version": "kibana",
- "ui": true
+ "ui": true,
+ "requiredBundles": ["kibanaUtils"]
}
diff --git a/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.test.tsx b/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.test.tsx
index 8f264a6bafca7..03af32712afa5 100644
--- a/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.test.tsx
+++ b/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.test.tsx
@@ -20,7 +20,7 @@
import React from 'react';
import sinon from 'sinon';
import { ExitFullScreenButton } from './exit_full_screen_button';
-import { keyCodes } from '@elastic/eui';
+import { keys } from '@elastic/eui';
import { mount } from 'enzyme';
test('is rendered', () => {
@@ -45,7 +45,7 @@ describe('onExitFullScreenMode', () => {
mount( );
- const escapeKeyEvent = new KeyboardEvent('keydown', { keyCode: keyCodes.ESCAPE } as any);
+ const escapeKeyEvent = new KeyboardEvent('keydown', { key: keys.ESCAPE } as any);
document.dispatchEvent(escapeKeyEvent);
sinon.assert.calledOnce(onExitHandler);
diff --git a/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.tsx b/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.tsx
index 2a359b7cca5d1..3a1a34f1fc3be 100644
--- a/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.tsx
+++ b/src/plugins/kibana_react/public/exit_full_screen_button/exit_full_screen_button.tsx
@@ -19,7 +19,7 @@
import { i18n } from '@kbn/i18n';
import React, { PureComponent } from 'react';
-import { EuiScreenReaderOnly, keyCodes } from '@elastic/eui';
+import { EuiScreenReaderOnly, keys } from '@elastic/eui';
import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
export interface ExitFullScreenButtonProps {
@@ -30,7 +30,7 @@ import './index.scss';
class ExitFullScreenButtonUi extends PureComponent {
public onKeyDown = (e: KeyboardEvent) => {
- if (e.keyCode === keyCodes.ESCAPE) {
+ if (e.key === keys.ESCAPE) {
this.props.onExitFullScreenMode();
}
};
diff --git a/src/plugins/kibana_react/public/split_panel/containers/panel_container.tsx b/src/plugins/kibana_react/public/split_panel/containers/panel_container.tsx
index 45fe20095fd83..a44ed04c7bc79 100644
--- a/src/plugins/kibana_react/public/split_panel/containers/panel_container.tsx
+++ b/src/plugins/kibana_react/public/split_panel/containers/panel_container.tsx
@@ -19,7 +19,7 @@
import React, { Children, ReactNode, useRef, useState, useCallback, useEffect } from 'react';
-import { keyCodes } from '@elastic/eui';
+import { keys } from '@elastic/eui';
import { PanelContextProvider } from '../context';
import { Resizer, ResizerMouseEvent, ResizerKeyDownEvent } from '../components/resizer';
import { PanelRegistry } from '../registry';
@@ -70,16 +70,16 @@ export function PanelsContainer({
const handleKeyDown = useCallback(
(ev: ResizerKeyDownEvent) => {
- const { keyCode } = ev;
+ const { key } = ev;
- if (keyCode === keyCodes.LEFT || keyCode === keyCodes.RIGHT) {
+ if (key === keys.ARROW_LEFT || key === keys.ARROW_RIGHT) {
ev.preventDefault();
const { current: registry } = registryRef;
const [left, right] = registry.getPanels();
- const leftPercent = left.width - (keyCode === keyCodes.LEFT ? 1 : -1);
- const rightPercent = right.width - (keyCode === keyCodes.RIGHT ? 1 : -1);
+ const leftPercent = left.width - (key === keys.ARROW_LEFT ? 1 : -1);
+ const rightPercent = right.width - (key === keys.ARROW_RIGHT ? 1 : -1);
left.setWidth(leftPercent);
right.setWidth(rightPercent);
diff --git a/src/plugins/management/kibana.json b/src/plugins/management/kibana.json
index cc411a8c6a25c..f48158e98ff3f 100644
--- a/src/plugins/management/kibana.json
+++ b/src/plugins/management/kibana.json
@@ -3,5 +3,6 @@
"version": "kibana",
"server": true,
"ui": true,
- "requiredPlugins": ["kibanaLegacy", "home"]
+ "requiredPlugins": ["kibanaLegacy", "home"],
+ "requiredBundles": ["kibanaReact"]
}
diff --git a/src/plugins/maps_legacy/kibana.json b/src/plugins/maps_legacy/kibana.json
index cd503883164ac..d9bf33e661368 100644
--- a/src/plugins/maps_legacy/kibana.json
+++ b/src/plugins/maps_legacy/kibana.json
@@ -4,5 +4,6 @@
"kibanaVersion": "kibana",
"configPath": ["map"],
"ui": true,
- "server": true
+ "server": true,
+ "requiredBundles": ["kibanaReact", "charts"]
}
diff --git a/src/plugins/region_map/kibana.json b/src/plugins/region_map/kibana.json
index ac7e1f8659d66..6e1980c327dc0 100644
--- a/src/plugins/region_map/kibana.json
+++ b/src/plugins/region_map/kibana.json
@@ -11,5 +11,10 @@
"mapsLegacy",
"kibanaLegacy",
"data"
+ ],
+ "requiredBundles": [
+ "kibanaUtils",
+ "kibanaReact",
+ "charts"
]
}
diff --git a/src/plugins/saved_objects/kibana.json b/src/plugins/saved_objects/kibana.json
index 7ae1b84eecad8..589aafbd2aaf5 100644
--- a/src/plugins/saved_objects/kibana.json
+++ b/src/plugins/saved_objects/kibana.json
@@ -3,5 +3,9 @@
"version": "kibana",
"server": true,
"ui": true,
- "requiredPlugins": ["data"]
+ "requiredPlugins": ["data"],
+ "requiredBundles": [
+ "kibanaUtils",
+ "kibanaReact"
+ ]
}
diff --git a/src/plugins/saved_objects_management/kibana.json b/src/plugins/saved_objects_management/kibana.json
index 6184d890c415c..0270c1d8f5d39 100644
--- a/src/plugins/saved_objects_management/kibana.json
+++ b/src/plugins/saved_objects_management/kibana.json
@@ -5,5 +5,6 @@
"ui": true,
"requiredPlugins": ["home", "management", "data"],
"optionalPlugins": ["dashboard", "visualizations", "discover"],
- "extraPublicDirs": ["public/lib"]
+ "extraPublicDirs": ["public/lib"],
+ "requiredBundles": ["kibanaReact"]
}
diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx
index 6b209a62e1b98..6256e5fcd49c5 100644
--- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx
+++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/table.test.tsx
@@ -21,7 +21,7 @@ import React from 'react';
import { shallowWithI18nProvider, mountWithI18nProvider } from 'test_utils/enzyme_helpers';
// @ts-expect-error
import { findTestSubject } from '@elastic/eui/lib/test';
-import { keyCodes } from '@elastic/eui';
+import { keys } from '@elastic/eui';
import { httpServiceMock } from '../../../../../../core/public/mocks';
import { actionServiceMock } from '../../../services/action_service.mock';
import { Table, TableProps } from './table';
@@ -100,14 +100,14 @@ describe('Table', () => {
const searchBar = findTestSubject(component, 'savedObjectSearchBar');
// Send invalid query
- searchBar.simulate('keyup', { keyCode: keyCodes.ENTER, target: { value: '?' } });
+ searchBar.simulate('keyup', { key: keys.ENTER, target: { value: '?' } });
expect(onQueryChangeMock).toHaveBeenCalledTimes(0);
expect(component.state().isSearchTextValid).toBe(false);
onQueryChangeMock.mockReset();
// Send valid query to ensure component can recover from invalid query
- searchBar.simulate('keyup', { keyCode: keyCodes.ENTER, target: { value: 'I am valid' } });
+ searchBar.simulate('keyup', { key: keys.ENTER, target: { value: 'I am valid' } });
expect(onQueryChangeMock).toHaveBeenCalledTimes(1);
expect(component.state().isSearchTextValid).toBe(true);
});
diff --git a/src/plugins/share/kibana.json b/src/plugins/share/kibana.json
index dce2ac9281aba..7760ea321992d 100644
--- a/src/plugins/share/kibana.json
+++ b/src/plugins/share/kibana.json
@@ -2,5 +2,6 @@
"id": "share",
"version": "kibana",
"server": true,
- "ui": true
+ "ui": true,
+ "requiredBundles": ["kibanaUtils"]
}
diff --git a/src/plugins/telemetry/kibana.json b/src/plugins/telemetry/kibana.json
index a497597762520..520ca6076dbbd 100644
--- a/src/plugins/telemetry/kibana.json
+++ b/src/plugins/telemetry/kibana.json
@@ -9,5 +9,9 @@
],
"extraPublicDirs": [
"common/constants"
+ ],
+ "requiredBundles": [
+ "kibanaUtils",
+ "kibanaReact"
]
}
diff --git a/src/plugins/tile_map/kibana.json b/src/plugins/tile_map/kibana.json
index bb8ef5a246549..9881a2dd72308 100644
--- a/src/plugins/tile_map/kibana.json
+++ b/src/plugins/tile_map/kibana.json
@@ -11,5 +11,10 @@
"mapsLegacy",
"kibanaLegacy",
"data"
+ ],
+ "requiredBundles": [
+ "kibanaUtils",
+ "kibanaReact",
+ "charts"
]
}
diff --git a/src/plugins/ui_actions/kibana.json b/src/plugins/ui_actions/kibana.json
index 907cbabbdf9c9..7b24b3cc5c48b 100644
--- a/src/plugins/ui_actions/kibana.json
+++ b/src/plugins/ui_actions/kibana.json
@@ -5,5 +5,8 @@
"ui": true,
"extraPublicDirs": [
"public/tests/test_samples"
+ ],
+ "requiredBundles": [
+ "kibanaReact"
]
}
diff --git a/src/plugins/usage_collection/kibana.json b/src/plugins/usage_collection/kibana.json
index ae86b6c5d7ad1..6ef78018c7d7f 100644
--- a/src/plugins/usage_collection/kibana.json
+++ b/src/plugins/usage_collection/kibana.json
@@ -3,5 +3,8 @@
"configPath": ["usageCollection"],
"version": "kibana",
"server": true,
- "ui": true
+ "ui": true,
+ "requiredBundles": [
+ "kibanaUtils"
+ ]
}
diff --git a/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx b/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx
index c41315e7bc0dc..bcbc5afec1fdc 100644
--- a/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx
+++ b/src/plugins/vis_default_editor/public/components/sidebar/sidebar.tsx
@@ -20,7 +20,7 @@
import React, { useMemo, useState, useCallback, KeyboardEventHandler, useEffect } from 'react';
import { get, isEqual } from 'lodash';
import { i18n } from '@kbn/i18n';
-import { keyCodes, EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { keys, EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { EventEmitter } from 'events';
import {
@@ -119,7 +119,7 @@ function DefaultEditorSideBar({
const onSubmit: KeyboardEventHandler = useCallback(
(event) => {
- if (event.ctrlKey && event.keyCode === keyCodes.ENTER) {
+ if (event.ctrlKey && event.key === keys.ENTER) {
event.preventDefault();
event.stopPropagation();
diff --git a/src/plugins/vis_type_markdown/kibana.json b/src/plugins/vis_type_markdown/kibana.json
index d52e22118ccf0..9241f5eeee837 100644
--- a/src/plugins/vis_type_markdown/kibana.json
+++ b/src/plugins/vis_type_markdown/kibana.json
@@ -3,5 +3,6 @@
"version": "kibana",
"ui": true,
"server": true,
- "requiredPlugins": ["expressions", "visualizations"]
+ "requiredPlugins": ["expressions", "visualizations"],
+ "requiredBundles": ["kibanaUtils", "kibanaReact", "data", "charts"]
}
diff --git a/src/plugins/vis_type_metric/kibana.json b/src/plugins/vis_type_metric/kibana.json
index 24135d257b317..b2ebc91471e9d 100644
--- a/src/plugins/vis_type_metric/kibana.json
+++ b/src/plugins/vis_type_metric/kibana.json
@@ -4,5 +4,6 @@
"kibanaVersion": "kibana",
"server": true,
"ui": true,
- "requiredPlugins": ["data", "visualizations", "charts","expressions"]
+ "requiredPlugins": ["data", "visualizations", "charts","expressions"],
+ "requiredBundles": ["kibanaUtils", "kibanaReact"]
}
diff --git a/src/plugins/vis_type_metric/public/components/metric_vis_value.tsx b/src/plugins/vis_type_metric/public/components/metric_vis_value.tsx
index 79876377c8e44..267d92abe2c75 100644
--- a/src/plugins/vis_type_metric/public/components/metric_vis_value.tsx
+++ b/src/plugins/vis_type_metric/public/components/metric_vis_value.tsx
@@ -20,7 +20,7 @@
import React, { Component, KeyboardEvent } from 'react';
import classNames from 'classnames';
-import { EuiKeyboardAccessible, keyCodes } from '@elastic/eui';
+import { EuiKeyboardAccessible, keys } from '@elastic/eui';
import { MetricVisMetric } from '../types';
@@ -39,7 +39,7 @@ export class MetricVisValue extends Component {
};
onKeyPress = (event: KeyboardEvent) => {
- if (event.keyCode === keyCodes.ENTER) {
+ if (event.key === keys.ENTER) {
this.onClick();
}
};
diff --git a/src/plugins/vis_type_table/kibana.json b/src/plugins/vis_type_table/kibana.json
index ed098d7161403..b3c1556429077 100644
--- a/src/plugins/vis_type_table/kibana.json
+++ b/src/plugins/vis_type_table/kibana.json
@@ -8,5 +8,11 @@
"visualizations",
"data",
"kibanaLegacy"
+ ],
+ "requiredBundles": [
+ "kibanaUtils",
+ "kibanaReact",
+ "share",
+ "charts"
]
}
diff --git a/src/plugins/vis_type_tagcloud/kibana.json b/src/plugins/vis_type_tagcloud/kibana.json
index dbc9a1b9ef692..86f72ebfa936d 100644
--- a/src/plugins/vis_type_tagcloud/kibana.json
+++ b/src/plugins/vis_type_tagcloud/kibana.json
@@ -3,5 +3,6 @@
"version": "kibana",
"ui": true,
"server": true,
- "requiredPlugins": ["data", "expressions", "visualizations", "charts"]
+ "requiredPlugins": ["data", "expressions", "visualizations", "charts"],
+ "requiredBundles": ["kibanaUtils", "kibanaReact"]
}
diff --git a/src/plugins/vis_type_timelion/kibana.json b/src/plugins/vis_type_timelion/kibana.json
index 85c282c51a2e7..6946568f5d809 100644
--- a/src/plugins/vis_type_timelion/kibana.json
+++ b/src/plugins/vis_type_timelion/kibana.json
@@ -4,5 +4,6 @@
"kibanaVersion": "kibana",
"server": true,
"ui": true,
- "requiredPlugins": ["visualizations", "data", "expressions"]
+ "requiredPlugins": ["visualizations", "data", "expressions"],
+ "requiredBundles": ["kibanaUtils", "kibanaReact"]
}
diff --git a/src/plugins/vis_type_timeseries/kibana.json b/src/plugins/vis_type_timeseries/kibana.json
index 9053d2543e0d0..f2284726c463f 100644
--- a/src/plugins/vis_type_timeseries/kibana.json
+++ b/src/plugins/vis_type_timeseries/kibana.json
@@ -5,5 +5,6 @@
"server": true,
"ui": true,
"requiredPlugins": ["charts", "data", "expressions", "visualizations"],
- "optionalPlugins": ["usageCollection"]
+ "optionalPlugins": ["usageCollection"],
+ "requiredBundles": ["kibanaUtils", "kibanaReact"]
}
diff --git a/src/plugins/vis_type_timeseries/public/application/components/color_rules.test.js b/src/plugins/vis_type_timeseries/public/application/components/color_rules.test.js
index eae354f7cc8ec..db6024d48be12 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/color_rules.test.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/color_rules.test.js
@@ -20,7 +20,7 @@
import React from 'react';
import { collectionActions } from './lib/collection_actions';
import { ColorRules } from './color_rules';
-import { keyCodes } from '@elastic/eui';
+import { keys } from '@elastic/eui';
import { findTestSubject } from '@elastic/eui/lib/test';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
@@ -63,9 +63,9 @@ describe('src/legacy/core_plugins/metrics/public/components/color_rules.test.js'
collectionActions.handleChange = jest.fn();
const wrapper = mountWithIntl( );
const operatorInput = findTestSubject(wrapper, 'colorRuleOperator');
- operatorInput.simulate('keyDown', { keyCode: keyCodes.DOWN });
- operatorInput.simulate('keyDown', { keyCode: keyCodes.DOWN });
- operatorInput.simulate('keyDown', { keyCode: keyCodes.ENTER });
+ operatorInput.simulate('keyDown', { key: keys.ARROW_DOWN });
+ operatorInput.simulate('keyDown', { key: keys.ARROW_DOWN });
+ operatorInput.simulate('keyDown', { key: keys.ENTER });
expect(collectionActions.handleChange.mock.calls[0][1].operator).toEqual('gt');
const numberInput = findTestSubject(wrapper, 'colorRuleValue');
diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js b/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js
index 23a9555da2452..9c2b947bda08e 100644
--- a/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js
+++ b/src/plugins/vis_type_timeseries/public/application/components/vis_editor_visualization.js
@@ -19,7 +19,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { get } from 'lodash';
-import { keyCodes, EuiFlexGroup, EuiFlexItem, EuiButton, EuiText, EuiSwitch } from '@elastic/eui';
+import { keys, EuiFlexGroup, EuiFlexItem, EuiButton, EuiText, EuiSwitch } from '@elastic/eui';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
import {
getInterval,
@@ -96,11 +96,11 @@ class VisEditorVisualizationUI extends Component {
* defined minimum width (MIN_CHART_HEIGHT).
*/
onSizeHandleKeyDown = (ev) => {
- const { keyCode } = ev;
- if (keyCode === keyCodes.UP || keyCode === keyCodes.DOWN) {
+ const { key } = ev;
+ if (key === keys.ARROW_UP || key === keys.ARROW_DOWN) {
ev.preventDefault();
this.setState((prevState) => {
- const newHeight = prevState.height + (keyCode === keyCodes.UP ? -15 : 15);
+ const newHeight = prevState.height + (key === keys.ARROW_UP ? -15 : 15);
return {
height: Math.max(MIN_CHART_HEIGHT, newHeight),
};
diff --git a/src/plugins/vis_type_vega/kibana.json b/src/plugins/vis_type_vega/kibana.json
index f1f82e7f5b7ad..d7a92de627a99 100644
--- a/src/plugins/vis_type_vega/kibana.json
+++ b/src/plugins/vis_type_vega/kibana.json
@@ -3,5 +3,6 @@
"version": "kibana",
"server": true,
"ui": true,
- "requiredPlugins": ["data", "visualizations", "mapsLegacy", "expressions"]
+ "requiredPlugins": ["data", "visualizations", "mapsLegacy", "expressions"],
+ "requiredBundles": ["kibanaUtils", "kibanaReact"]
}
diff --git a/src/plugins/vis_type_vislib/kibana.json b/src/plugins/vis_type_vislib/kibana.json
index cad0ebe01494a..7cba2e0d6a6b4 100644
--- a/src/plugins/vis_type_vislib/kibana.json
+++ b/src/plugins/vis_type_vislib/kibana.json
@@ -4,5 +4,6 @@
"server": true,
"ui": true,
"requiredPlugins": ["charts", "data", "expressions", "visualizations", "kibanaLegacy"],
- "optionalPlugins": ["visTypeXy"]
+ "optionalPlugins": ["visTypeXy"],
+ "requiredBundles": ["kibanaUtils", "kibanaReact"]
}
diff --git a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx
index f7e44ed278787..129fdd2ade9bd 100644
--- a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx
+++ b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend.tsx
@@ -21,7 +21,7 @@ import classNames from 'classnames';
import { compact, uniqBy, map, every, isUndefined } from 'lodash';
import { i18n } from '@kbn/i18n';
-import { EuiPopoverProps, EuiIcon, keyCodes, htmlIdGenerator } from '@elastic/eui';
+import { EuiPopoverProps, EuiIcon, keys, htmlIdGenerator } from '@elastic/eui';
import { getDataActions } from '../../../services';
import { CUSTOM_LEGEND_VIS_TYPES, LegendItem } from './models';
@@ -75,7 +75,7 @@ export class VisLegend extends PureComponent {
};
setColor = (label: string, color: string) => (event: BaseSyntheticEvent) => {
- if ((event as KeyboardEvent).keyCode && (event as KeyboardEvent).keyCode !== keyCodes.ENTER) {
+ if ((event as KeyboardEvent).key && (event as KeyboardEvent).key !== keys.ENTER) {
return;
}
@@ -106,11 +106,7 @@ export class VisLegend extends PureComponent {
};
toggleDetails = (label: string | null) => (event?: BaseSyntheticEvent) => {
- if (
- event &&
- (event as KeyboardEvent).keyCode &&
- (event as KeyboardEvent).keyCode !== keyCodes.ENTER
- ) {
+ if (event && (event as KeyboardEvent).key && (event as KeyboardEvent).key !== keys.ENTER) {
return;
}
this.setState({ selectedLabel: this.state.selectedLabel === label ? null : label });
diff --git a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend_item.tsx b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend_item.tsx
index 70b7a8ee335db..b440384899d5f 100644
--- a/src/plugins/vis_type_vislib/public/vislib/components/legend/legend_item.tsx
+++ b/src/plugins/vis_type_vislib/public/vislib/components/legend/legend_item.tsx
@@ -24,7 +24,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiPopover,
- keyCodes,
+ keys,
EuiIcon,
EuiSpacer,
EuiButtonEmpty,
@@ -67,7 +67,7 @@ const VisLegendItemComponent = ({
* This will close the details panel of this legend entry when pressing Escape.
*/
const onLegendEntryKeydown = (event: KeyboardEvent) => {
- if (event.keyCode === keyCodes.ESCAPE) {
+ if (event.key === keys.ESCAPE) {
event.preventDefault();
event.stopPropagation();
onSelect(null)();
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/chart_title.js b/src/plugins/vis_type_vislib/public/vislib/lib/chart_title.test.js
similarity index 73%
rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/chart_title.js
rename to src/plugins/vis_type_vislib/public/vislib/lib/chart_title.test.js
index 6790c49691dfd..d8d5087f8c380 100644
--- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/chart_title.js
+++ b/src/plugins/vis_type_vislib/public/vislib/lib/chart_title.test.js
@@ -19,11 +19,15 @@
import d3 from 'd3';
import _ from 'lodash';
-import expect from '@kbn/expect';
+import {
+ setHTMLElementClientSizes,
+ setSVGElementGetBBox,
+ setSVGElementGetComputedTextLength,
+} from '../../../../../test_utils/public';
-import { ChartTitle } from '../../../../../../../plugins/vis_type_vislib/public/vislib/lib/chart_title';
-import { VisConfig } from '../../../../../../../plugins/vis_type_vislib/public/vislib/lib/vis_config';
-import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks';
+import { ChartTitle } from './chart_title';
+import { VisConfig } from './vis_config';
+import { getMockUiState } from '../../fixtures/mocks';
describe('Vislib ChartTitle Class Test Suite', function () {
let mockUiState;
@@ -88,6 +92,16 @@ describe('Vislib ChartTitle Class Test Suite', function () {
yAxisLabel: 'Count',
};
+ let mockedHTMLElementClientSizes;
+ let mockedSVGElementGetBBox;
+ let mockedSVGElementGetComputedTextLength;
+
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100);
+ });
+
beforeEach(() => {
mockUiState = getMockUiState();
el = d3.select('body').append('div').attr('class', 'visWrapper').datum(data);
@@ -113,23 +127,29 @@ describe('Vislib ChartTitle Class Test Suite', function () {
el.remove();
});
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ mockedSVGElementGetComputedTextLength.mockRestore();
+ });
+
describe('render Method', function () {
beforeEach(function () {
chartTitle.render();
});
- it('should append an svg to div', function () {
- expect(el.select('.chart-title').selectAll('svg').length).to.be(1);
+ test('should append an svg to div', function () {
+ expect(el.select('.chart-title').selectAll('svg').length).toBe(1);
});
- it('should append text', function () {
- expect(!!el.select('.chart-title').selectAll('svg').selectAll('text')).to.be(true);
+ test('should append text', function () {
+ expect(!!el.select('.chart-title').selectAll('svg').selectAll('text')).toBe(true);
});
});
describe('draw Method', function () {
- it('should be a function', function () {
- expect(_.isFunction(chartTitle.draw())).to.be(true);
+ test('should be a function', function () {
+ expect(_.isFunction(chartTitle.draw())).toBe(true);
});
});
});
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/dispatch.js b/src/plugins/vis_type_vislib/public/vislib/lib/dispatch.test.js
similarity index 67%
rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/dispatch.js
rename to src/plugins/vis_type_vislib/public/vislib/lib/dispatch.test.js
index 20281d8479ab4..9c714af4d8434 100644
--- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/dispatch.js
+++ b/src/plugins/vis_type_vislib/public/vislib/lib/dispatch.test.js
@@ -19,13 +19,21 @@
import _ from 'lodash';
import d3 from 'd3';
-import expect from '@kbn/expect';
+import {
+ setHTMLElementClientSizes,
+ setSVGElementGetBBox,
+ setSVGElementGetComputedTextLength,
+} from '../../../../../test_utils/public';
// Data
-import data from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series';
+import data from '../../fixtures/mock_data/date_histogram/_series';
-import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks';
-import { getVis } from '../_vis_fixture';
+import { getMockUiState } from '../../fixtures/mocks';
+import { getVis } from '../visualizations/_vis_fixture';
+
+let mockedHTMLElementClientSizes;
+let mockedSVGElementGetBBox;
+let mockedSVGElementGetComputedTextLength;
describe('Vislib Dispatch Class Test Suite', function () {
function destroyVis(vis) {
@@ -36,6 +44,18 @@ describe('Vislib Dispatch Class Test Suite', function () {
return d3.select(element).data(new Array(n)).enter().append(type);
}
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100);
+ });
+
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ mockedSVGElementGetComputedTextLength.mockRestore();
+ });
+
describe('', function () {
let vis;
let mockUiState;
@@ -50,13 +70,13 @@ describe('Vislib Dispatch Class Test Suite', function () {
destroyVis(vis);
});
- it('implements on, off, emit methods', function () {
+ test('implements on, off, emit methods', function () {
const events = _.map(vis.handler.charts, 'events');
- expect(events.length).to.be.above(0);
+ expect(events.length).toBeGreaterThan(0);
events.forEach(function (dispatch) {
- expect(dispatch).to.have.property('on');
- expect(dispatch).to.have.property('off');
- expect(dispatch).to.have.property('emit');
+ expect(dispatch).toHaveProperty('on');
+ expect(dispatch).toHaveProperty('off');
+ expect(dispatch).toHaveProperty('emit');
});
});
});
@@ -77,15 +97,15 @@ describe('Vislib Dispatch Class Test Suite', function () {
});
describe('addEvent method', function () {
- it('returns a function that binds the passed event to a selection', function () {
+ test('returns a function that binds the passed event to a selection', function () {
const chart = _.first(vis.handler.charts);
const apply = chart.events.addEvent('event', _.noop);
- expect(apply).to.be.a('function');
+ expect(apply).toBeInstanceOf(Function);
const els = getEls(vis.element, 3, 'div');
apply(els);
els.each(function () {
- expect(d3.select(this).on('event')).to.be(_.noop);
+ expect(d3.select(this).on('event')).toBe(_.noop);
});
});
});
@@ -94,21 +114,21 @@ describe('Vislib Dispatch Class Test Suite', function () {
// checking that they return function which bind the events expected
function checkBoundAddMethod(name, event) {
describe(name + ' method', function () {
- it('should be a function', function () {
+ test('should be a function', function () {
vis.handler.charts.forEach(function (chart) {
- expect(chart.events[name]).to.be.a('function');
+ expect(chart.events[name]).toBeInstanceOf(Function);
});
});
- it('returns a function that binds ' + event + ' events to a selection', function () {
+ test('returns a function that binds ' + event + ' events to a selection', function () {
const chart = _.first(vis.handler.charts);
const apply = chart.events[name](chart.series[0].chartEl);
- expect(apply).to.be.a('function');
+ expect(apply).toBeInstanceOf(Function);
const els = getEls(vis.element, 3, 'div');
apply(els);
els.each(function () {
- expect(d3.select(this).on(event)).to.be.a('function');
+ expect(d3.select(this).on(event)).toBeInstanceOf(Function);
});
});
});
@@ -119,26 +139,26 @@ describe('Vislib Dispatch Class Test Suite', function () {
checkBoundAddMethod('addClickEvent', 'click');
describe('addMousePointer method', function () {
- it('should be a function', function () {
+ test('should be a function', function () {
vis.handler.charts.forEach(function (chart) {
const pointer = chart.events.addMousePointer;
- expect(_.isFunction(pointer)).to.be(true);
+ expect(_.isFunction(pointer)).toBe(true);
});
});
});
describe('clickEvent handler', () => {
describe('for pie chart', () => {
- it('prepares data points', () => {
+ test('prepares data points', () => {
const expectedResponse = [{ column: 0, row: 0, table: {}, value: 0 }];
const d = { rawData: { column: 0, row: 0, table: {}, value: 0 } };
const chart = _.first(vis.handler.charts);
const response = chart.events.clickEventResponse(d, { isSlices: true });
- expect(response.data).to.eql(expectedResponse);
+ expect(response.data).toEqual(expectedResponse);
});
- it('remove invalid points', () => {
+ test('remove invalid points', () => {
const expectedResponse = [{ column: 0, row: 0, table: {}, value: 0 }];
const d = {
rawData: { column: 0, row: 0, table: {}, value: 0 },
@@ -146,20 +166,20 @@ describe('Vislib Dispatch Class Test Suite', function () {
};
const chart = _.first(vis.handler.charts);
const response = chart.events.clickEventResponse(d, { isSlices: true });
- expect(response.data).to.eql(expectedResponse);
+ expect(response.data).toEqual(expectedResponse);
});
});
describe('for xy charts', () => {
- it('prepares data points', () => {
+ test('prepares data points', () => {
const expectedResponse = [{ column: 0, row: 0, table: {}, value: 0 }];
const d = { xRaw: { column: 0, row: 0, table: {}, value: 0 } };
const chart = _.first(vis.handler.charts);
const response = chart.events.clickEventResponse(d, { isSlices: false });
- expect(response.data).to.eql(expectedResponse);
+ expect(response.data).toEqual(expectedResponse);
});
- it('remove invalid points', () => {
+ test('remove invalid points', () => {
const expectedResponse = [{ column: 0, row: 0, table: {}, value: 0 }];
const d = {
xRaw: { column: 0, row: 0, table: {}, value: 0 },
@@ -167,35 +187,35 @@ describe('Vislib Dispatch Class Test Suite', function () {
};
const chart = _.first(vis.handler.charts);
const response = chart.events.clickEventResponse(d, { isSlices: false });
- expect(response.data).to.eql(expectedResponse);
+ expect(response.data).toEqual(expectedResponse);
});
});
});
});
describe('Custom event handlers', function () {
- it('should attach whatever gets passed on vis.on() to chart.events', function (done) {
+ test('should attach whatever gets passed on vis.on() to chart.events', function (done) {
const vis = getVis();
const mockUiState = getMockUiState();
vis.on('someEvent', _.noop);
vis.render(data, mockUiState);
vis.handler.charts.forEach(function (chart) {
- expect(chart.events.listenerCount('someEvent')).to.be(1);
+ expect(chart.events.listenerCount('someEvent')).toBe(1);
});
destroyVis(vis);
done();
});
- it('can be added after rendering', function () {
+ test('can be added after rendering', function () {
const vis = getVis();
const mockUiState = getMockUiState();
vis.render(data, mockUiState);
vis.on('someEvent', _.noop);
vis.handler.charts.forEach(function (chart) {
- expect(chart.events.listenerCount('someEvent')).to.be(1);
+ expect(chart.events.listenerCount('someEvent')).toBe(1);
});
destroyVis(vis);
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/handler/handler.js b/src/plugins/vis_type_vislib/public/vislib/lib/handler.test.js
similarity index 58%
rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/handler/handler.js
rename to src/plugins/vis_type_vislib/public/vislib/lib/handler.test.js
index e4f75c47e621c..d50c70de1bb48 100644
--- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/handler/handler.js
+++ b/src/plugins/vis_type_vislib/public/vislib/lib/handler.test.js
@@ -17,25 +17,38 @@
* under the License.
*/
-import expect from '@kbn/expect';
import $ from 'jquery';
+import {
+ setHTMLElementClientSizes,
+ setSVGElementGetBBox,
+ setSVGElementGetComputedTextLength,
+} from '../../../../../test_utils/public';
// Data
-import series from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series';
-import columns from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_columns';
-import rows from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows';
-import stackedSeries from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series';
-import { getMockUiState } from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks';
-import { getVis } from '../../_vis_fixture';
+import series from '../../fixtures/mock_data/date_histogram/_series';
+import columns from '../../fixtures/mock_data/date_histogram/_columns';
+import rows from '../../fixtures/mock_data/date_histogram/_rows';
+import stackedSeries from '../../fixtures/mock_data/date_histogram/_stacked_series';
+import { getMockUiState } from '../../fixtures/mocks';
+import { getVis } from '../visualizations/_vis_fixture';
const dateHistogramArray = [series, columns, rows, stackedSeries];
const names = ['series', 'columns', 'rows', 'stackedSeries'];
+let mockedHTMLElementClientSizes;
+let mockedSVGElementGetBBox;
+let mockedSVGElementGetComputedTextLength;
dateHistogramArray.forEach(function (data, i) {
describe('Vislib Handler Test Suite for ' + names[i] + ' Data', function () {
const events = ['click', 'brush'];
let vis;
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100);
+ });
+
beforeEach(() => {
vis = getVis();
vis.render(data, getMockUiState());
@@ -45,11 +58,17 @@ dateHistogramArray.forEach(function (data, i) {
vis.destroy();
});
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ mockedSVGElementGetComputedTextLength.mockRestore();
+ });
+
describe('render Method', function () {
- it('should render charts', function () {
- expect(vis.handler.charts.length).to.be.greaterThan(0);
+ test('should render charts', function () {
+ expect(vis.handler.charts.length).toBeGreaterThan(0);
vis.handler.charts.forEach(function (chart) {
- expect($(chart.chartEl).find('svg').length).to.be(1);
+ expect($(chart.chartEl).find('svg').length).toBe(1);
});
});
});
@@ -67,10 +86,10 @@ dateHistogramArray.forEach(function (data, i) {
});
});
- it('should add events to chart and emit to the Events class', function () {
+ test('should add events to chart and emit to the Events class', function () {
charts.forEach(function (chart) {
events.forEach(function (event) {
- expect(chart.events.listenerCount(event)).to.be.above(0);
+ expect(chart.events.listenerCount(event)).toBeGreaterThan(0);
});
});
});
@@ -89,10 +108,10 @@ dateHistogramArray.forEach(function (data, i) {
});
});
- it('should remove events from the chart', function () {
+ test('should remove events from the chart', function () {
charts.forEach(function (chart) {
events.forEach(function (event) {
- expect(chart.events.listenerCount(event)).to.be(0);
+ expect(chart.events.listenerCount(event)).toBe(0);
});
});
});
@@ -103,8 +122,8 @@ dateHistogramArray.forEach(function (data, i) {
vis.handler.removeAll(vis.element);
});
- it('should remove all DOM elements from the el', function () {
- expect($(vis.element).children().length).to.be(0);
+ test('should remove all DOM elements from the el', function () {
+ expect($(vis.element).children().length).toBe(0);
});
});
@@ -113,9 +132,9 @@ dateHistogramArray.forEach(function (data, i) {
vis.handler.error('This is an error!');
});
- it('should return an error classed DOM element with a text message', function () {
- expect($(vis.element).find('.error').length).to.be(1);
- expect($('.error h4').html()).to.be('This is an error!');
+ test('should return an error classed DOM element with a text message', function () {
+ expect($(vis.element).find('.error').length).toBe(1);
+ expect($('.error h4').html()).toBe('This is an error!');
});
});
@@ -124,21 +143,21 @@ dateHistogramArray.forEach(function (data, i) {
vis.handler.destroy();
});
- it('should destroy all the charts in the visualization', function () {
- expect(vis.handler.charts.length).to.be(0);
+ test('should destroy all the charts in the visualization', function () {
+ expect(vis.handler.charts.length).toBe(0);
});
});
describe('event proxying', function () {
- it('should only pass the original event object to downstream handlers', function (done) {
+ test('should only pass the original event object to downstream handlers', function (done) {
const event = {};
const chart = vis.handler.charts[0];
const mockEmitter = function () {
const args = Array.from(arguments);
- expect(args.length).to.be(2);
- expect(args[0]).to.be('click');
- expect(args[1]).to.be(event);
+ expect(args.length).toBe(2);
+ expect(args[0]).toBe('click');
+ expect(args[1]).toBe(event);
done();
};
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/layout/layout.js b/src/plugins/vis_type_vislib/public/vislib/lib/layout/layout.test.js
similarity index 55%
rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/layout/layout.js
rename to src/plugins/vis_type_vislib/public/vislib/lib/layout/layout.test.js
index 7ad962fefc341..824d7073d6db5 100644
--- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/lib/layout/layout.js
+++ b/src/plugins/vis_type_vislib/public/vislib/lib/layout/layout.test.js
@@ -18,22 +18,30 @@
*/
import d3 from 'd3';
-import expect from '@kbn/expect';
import $ from 'jquery';
+import {
+ setHTMLElementClientSizes,
+ setSVGElementGetBBox,
+ setSVGElementGetComputedTextLength,
+} from '../../../../../../test_utils/public';
// Data
-import series from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series';
-import columns from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_columns';
-import rows from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows';
-import stackedSeries from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series';
-import { getMockUiState } from '../../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks';
-import { Layout } from '../../../../../../../../plugins/vis_type_vislib/public/vislib/lib/layout/layout';
-import { VisConfig } from '../../../../../../../../plugins/vis_type_vislib/public/vislib/lib/vis_config';
-import { getVis } from '../../_vis_fixture';
+import series from '../../../fixtures/mock_data/date_histogram/_series';
+import columns from '../../../fixtures/mock_data/date_histogram/_columns';
+import rows from '../../../fixtures/mock_data/date_histogram/_rows';
+import stackedSeries from '../../../fixtures/mock_data/date_histogram/_stacked_series';
+import { getMockUiState } from '../../../fixtures/mocks';
+import { Layout } from './layout';
+import { VisConfig } from '../vis_config';
+import { getVis } from '../../visualizations/_vis_fixture';
const dateHistogramArray = [series, columns, rows, stackedSeries];
const names = ['series', 'columns', 'rows', 'stackedSeries'];
+let mockedHTMLElementClientSizes;
+let mockedSVGElementGetBBox;
+let mockedSVGElementGetComputedTextLength;
+
dateHistogramArray.forEach(function (data, i) {
describe('Vislib Layout Class Test Suite for ' + names[i] + ' Data', function () {
let vis;
@@ -41,6 +49,12 @@ dateHistogramArray.forEach(function (data, i) {
let numberOfCharts;
let testLayout;
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100);
+ });
+
beforeEach(() => {
vis = getVis();
mockUiState = getMockUiState();
@@ -52,19 +66,25 @@ dateHistogramArray.forEach(function (data, i) {
vis.destroy();
});
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ mockedSVGElementGetComputedTextLength.mockRestore();
+ });
+
describe('createLayout Method', function () {
- it('should append all the divs', function () {
- expect($(vis.element).find('.visWrapper').length).to.be(1);
- expect($(vis.element).find('.visAxis--y').length).to.be(2);
- expect($(vis.element).find('.visWrapper__column').length).to.be(1);
- expect($(vis.element).find('.visAxis__column--y').length).to.be(2);
- expect($(vis.element).find('.y-axis-title').length).to.be.above(0);
- expect($(vis.element).find('.visAxis__splitAxes--y').length).to.be(2);
- expect($(vis.element).find('.visAxis__spacer--y').length).to.be(4);
- expect($(vis.element).find('.visWrapper__chart').length).to.be(numberOfCharts);
- expect($(vis.element).find('.visAxis--x').length).to.be(2);
- expect($(vis.element).find('.visAxis__splitAxes--x').length).to.be(2);
- expect($(vis.element).find('.x-axis-title').length).to.be.above(0);
+ test('should append all the divs', function () {
+ expect($(vis.element).find('.visWrapper').length).toBe(1);
+ expect($(vis.element).find('.visAxis--y').length).toBe(2);
+ expect($(vis.element).find('.visWrapper__column').length).toBe(1);
+ expect($(vis.element).find('.visAxis__column--y').length).toBe(2);
+ expect($(vis.element).find('.y-axis-title').length).toBeGreaterThan(0);
+ expect($(vis.element).find('.visAxis__splitAxes--y').length).toBe(2);
+ expect($(vis.element).find('.visAxis__spacer--y').length).toBe(4);
+ expect($(vis.element).find('.visWrapper__chart').length).toBe(numberOfCharts);
+ expect($(vis.element).find('.visAxis--x').length).toBe(2);
+ expect($(vis.element).find('.visAxis__splitAxes--x').length).toBe(2);
+ expect($(vis.element).find('.x-axis-title').length).toBeGreaterThan(0);
});
});
@@ -82,44 +102,44 @@ dateHistogramArray.forEach(function (data, i) {
testLayout = new Layout(visConfig);
});
- it('should append a div with the correct class name', function () {
- expect($(vis.element).find('.chart').length).to.be(numberOfCharts);
+ test('should append a div with the correct class name', function () {
+ expect($(vis.element).find('.chart').length).toBe(numberOfCharts);
});
- it('should bind data to the DOM element', function () {
- expect(!!$(vis.element).find('.chart').data()).to.be(true);
+ test('should bind data to the DOM element', function () {
+ expect(!!$(vis.element).find('.chart').data()).toBe(true);
});
- it('should create children', function () {
- expect(typeof $(vis.element).find('.x-axis-div')).to.be('object');
+ test('should create children', function () {
+ expect(typeof $(vis.element).find('.x-axis-div')).toBe('object');
});
- it('should call split function when provided', function () {
- expect(typeof $(vis.element).find('.x-axis-div')).to.be('object');
+ test('should call split function when provided', function () {
+ expect(typeof $(vis.element).find('.x-axis-div')).toBe('object');
});
- it('should throw errors when incorrect arguments provided', function () {
+ test('should throw errors when incorrect arguments provided', function () {
expect(function () {
testLayout.layout({
parent: vis.element,
type: undefined,
class: 'chart',
});
- }).to.throwError();
+ }).toThrowError();
expect(function () {
testLayout.layout({
type: 'div',
class: 'chart',
});
- }).to.throwError();
+ }).toThrowError();
expect(function () {
testLayout.layout({
parent: 'histogram',
type: 'div',
});
- }).to.throwError();
+ }).toThrowError();
expect(function () {
testLayout.layout({
@@ -129,7 +149,7 @@ dateHistogramArray.forEach(function (data, i) {
},
class: 'chart',
});
- }).to.throwError();
+ }).toThrowError();
});
});
@@ -139,9 +159,9 @@ dateHistogramArray.forEach(function (data, i) {
vis.handler.layout.appendElem('.visChart', 'div', 'test');
});
- it('should append DOM element to el with a class name', function () {
- expect(typeof $(vis.element).find('.column')).to.be('object');
- expect(typeof $(vis.element).find('.test')).to.be('object');
+ test('should append DOM element to el with a class name', function () {
+ expect(typeof $(vis.element).find('.column')).toBe('object');
+ expect(typeof $(vis.element).find('.test')).toBe('object');
});
});
@@ -151,8 +171,8 @@ dateHistogramArray.forEach(function (data, i) {
vis.handler.layout.removeAll(vis.element);
});
- it('should remove all DOM elements from the el', function () {
- expect($(vis.element).children().length).to.be(0);
+ test('should remove all DOM elements from the el', function () {
+ expect($(vis.element).children().length).toBe(0);
});
});
});
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/vis.js b/src/plugins/vis_type_vislib/public/vislib/vis.test.js
similarity index 56%
rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/vis.js
rename to src/plugins/vis_type_vislib/public/vislib/vis.test.js
index 36decdc415ed8..0c4fac97ccb9c 100644
--- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/vis.js
+++ b/src/plugins/vis_type_vislib/public/vislib/vis.test.js
@@ -19,18 +19,25 @@
import _ from 'lodash';
import $ from 'jquery';
-import expect from '@kbn/expect';
-
-import series from '../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series';
-import columns from '../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_columns';
-import rows from '../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows';
-import stackedSeries from '../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series';
-import { getMockUiState } from '../../../../../../plugins/vis_type_vislib/public/fixtures/mocks';
-import { getVis } from './_vis_fixture';
+import {
+ setHTMLElementClientSizes,
+ setSVGElementGetBBox,
+ setSVGElementGetComputedTextLength,
+} from '../../../../test_utils/public';
+import series from '../fixtures/mock_data/date_histogram/_series';
+import columns from '../fixtures/mock_data/date_histogram/_columns';
+import rows from '../fixtures/mock_data/date_histogram/_rows';
+import stackedSeries from '../fixtures/mock_data/date_histogram/_stacked_series';
+import { getMockUiState } from '../fixtures/mocks';
+import { getVis } from './visualizations/_vis_fixture';
const dataArray = [series, columns, rows, stackedSeries];
const names = ['series', 'columns', 'rows', 'stackedSeries'];
+let mockedHTMLElementClientSizes;
+let mockedSVGElementGetBBox;
+let mockedSVGElementGetComputedTextLength;
+
dataArray.forEach(function (data, i) {
describe('Vislib Vis Test Suite for ' + names[i] + ' Data', function () {
const beforeEvent = 'click';
@@ -40,6 +47,12 @@ dataArray.forEach(function (data, i) {
let secondVis;
let numberOfCharts;
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100);
+ });
+
beforeEach(() => {
vis = getVis();
secondVis = getVis();
@@ -51,34 +64,40 @@ dataArray.forEach(function (data, i) {
secondVis.destroy();
});
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ mockedSVGElementGetComputedTextLength.mockRestore();
+ });
+
describe('render Method', function () {
beforeEach(function () {
vis.render(data, mockUiState);
numberOfCharts = vis.handler.charts.length;
});
- it('should bind data to this object', function () {
- expect(_.isObject(vis.data)).to.be(true);
+ test('should bind data to this object', function () {
+ expect(_.isObject(vis.data)).toBe(true);
});
- it('should instantiate a handler object', function () {
- expect(_.isObject(vis.handler)).to.be(true);
+ test('should instantiate a handler object', function () {
+ expect(_.isObject(vis.handler)).toBe(true);
});
- it('should append a chart', function () {
- expect($('.chart').length).to.be(numberOfCharts);
+ test('should append a chart', function () {
+ expect($('.chart').length).toBe(numberOfCharts);
});
- it('should throw an error if no data is provided', function () {
+ test('should throw an error if no data is provided', function () {
expect(function () {
vis.render(null, mockUiState);
- }).to.throwError();
+ }).toThrowError();
});
});
describe('getLegendColors method', () => {
- it('should return null if no colors are defined', () => {
- expect(vis.getLegendColors()).to.equal(null);
+ test('should return null if no colors are defined', () => {
+ expect(vis.getLegendColors()).toEqual(null);
});
});
@@ -89,12 +108,12 @@ dataArray.forEach(function (data, i) {
secondVis.destroy();
});
- it('should remove all DOM elements from el', function () {
- expect($(secondVis.el).find('.visWrapper').length).to.be(0);
+ test('should remove all DOM elements from el', function () {
+ expect($(secondVis.el).find('.visWrapper').length).toBe(0);
});
- it('should not remove visualizations that have not been destroyed', function () {
- expect($(vis.element).find('.visWrapper').length).to.be(1);
+ test('should not remove visualizations that have not been destroyed', function () {
+ expect($(vis.element).find('.visWrapper').length).toBe(1);
});
});
@@ -105,9 +124,9 @@ dataArray.forEach(function (data, i) {
vis.set('offset', 'wiggle');
});
- it('should set an attribute', function () {
- expect(vis.get('addLegend')).to.be(false);
- expect(vis.get('offset')).to.be('wiggle');
+ test('should set an attribute', function () {
+ expect(vis.get('addLegend')).toBe(false);
+ expect(vis.get('offset')).toBe('wiggle');
});
});
@@ -116,10 +135,10 @@ dataArray.forEach(function (data, i) {
vis.render(data, mockUiState);
});
- it('should get attribute values', function () {
- expect(vis.get('addLegend')).to.be(true);
- expect(vis.get('addTooltip')).to.be(true);
- expect(vis.get('type')).to.be('point_series');
+ test('should get attribute values', function () {
+ expect(vis.get('addLegend')).toBe(true);
+ expect(vis.get('addTooltip')).toBe(true);
+ expect(vis.get('type')).toBe('point_series');
});
});
@@ -148,22 +167,22 @@ dataArray.forEach(function (data, i) {
vis.removeAllListeners(afterEvent);
});
- it('should add an event and its listeners', function () {
+ test('should add an event and its listeners', function () {
listeners.forEach(function (listener) {
- expect(vis.listeners(beforeEvent)).to.contain(listener);
+ expect(vis.listeners(beforeEvent)).toContain(listener);
});
listeners.forEach(function (listener) {
- expect(vis.listeners(afterEvent)).to.contain(listener);
+ expect(vis.listeners(afterEvent)).toContain(listener);
});
});
- it('should cause a listener for each event to be attached to each chart', function () {
+ test('should cause a listener for each event to be attached to each chart', function () {
const charts = vis.handler.charts;
charts.forEach(function (chart) {
- expect(chart.events.listenerCount(beforeEvent)).to.be.above(0);
- expect(chart.events.listenerCount(afterEvent)).to.be.above(0);
+ expect(chart.events.listenerCount(beforeEvent)).toBeGreaterThan(0);
+ expect(chart.events.listenerCount(afterEvent)).toBeGreaterThan(0);
});
});
});
@@ -205,45 +224,45 @@ dataArray.forEach(function (data, i) {
vis.removeAllListeners(afterEvent);
});
- it('should remove a listener', function () {
+ test('should remove a listener', function () {
const charts = vis.handler.charts;
- expect(vis.listeners(beforeEvent)).to.not.contain(listener1);
- expect(vis.listeners(beforeEvent)).to.contain(listener2);
+ expect(vis.listeners(beforeEvent)).not.toContain(listener1);
+ expect(vis.listeners(beforeEvent)).toContain(listener2);
- expect(vis.listeners(afterEvent)).to.not.contain(listener1);
- expect(vis.listeners(afterEvent)).to.contain(listener2);
+ expect(vis.listeners(afterEvent)).not.toContain(listener1);
+ expect(vis.listeners(afterEvent)).toContain(listener2);
// Events should still be attached to charts
charts.forEach(function (chart) {
- expect(chart.events.listenerCount(beforeEvent)).to.be.above(0);
- expect(chart.events.listenerCount(afterEvent)).to.be.above(0);
+ expect(chart.events.listenerCount(beforeEvent)).toBeGreaterThan(0);
+ expect(chart.events.listenerCount(afterEvent)).toBeGreaterThan(0);
});
});
- it('should remove the event and all listeners when only event passed an argument', function () {
+ test('should remove the event and all listeners when only event passed an argument', function () {
const charts = vis.handler.charts;
vis.removeAllListeners(afterEvent);
// should remove 'brush' event
- expect(vis.listeners(beforeEvent)).to.contain(listener2);
- expect(vis.listeners(afterEvent)).to.not.contain(listener2);
+ expect(vis.listeners(beforeEvent)).toContain(listener2);
+ expect(vis.listeners(afterEvent)).not.toContain(listener2);
// should remove the event from the charts
charts.forEach(function (chart) {
- expect(chart.events.listenerCount(beforeEvent)).to.be.above(0);
- expect(chart.events.listenerCount(afterEvent)).to.be(0);
+ expect(chart.events.listenerCount(beforeEvent)).toBeGreaterThan(0);
+ expect(chart.events.listenerCount(afterEvent)).toBe(0);
});
});
- it('should remove the event from the chart when the last listener is removed', function () {
+ test('should remove the event from the chart when the last listener is removed', function () {
const charts = vis.handler.charts;
vis.off(afterEvent, listener2);
- expect(vis.listenerCount(afterEvent)).to.be(0);
+ expect(vis.listenerCount(afterEvent)).toBe(0);
charts.forEach(function (chart) {
- expect(chart.events.listenerCount(afterEvent)).to.be(0);
+ expect(chart.events.listenerCount(afterEvent)).toBe(0);
});
});
});
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/_vis_fixture.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/_vis_fixture.js
similarity index 83%
rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/_vis_fixture.js
rename to src/plugins/vis_type_vislib/public/vislib/visualizations/_vis_fixture.js
index 7a68e847f13b1..0ffa53fc7ca9c 100644
--- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/_vis_fixture.js
+++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/_vis_fixture.js
@@ -19,11 +19,10 @@
import _ from 'lodash';
import $ from 'jquery';
+import { coreMock } from '../../../../../core/public/mocks';
+import { chartPluginMock } from '../../../../charts/public/mocks';
-import { Vis } from '../../../../../../plugins/vis_type_vislib/public/vislib/vis';
-
-// TODO: Remove when converted to jest mocks
-import { ColorsService } from '../../../../../../plugins/charts/public/services';
+import { Vis } from '../vis';
const $visCanvas = $('')
.attr('id', 'vislib-vis-fixtures')
@@ -55,15 +54,12 @@ afterEach(function () {
});
const getDeps = () => {
- const uiSettings = new Map();
- const colors = new ColorsService();
- colors.init(uiSettings);
+ const mockUiSettings = coreMock.createSetup().uiSettings;
+ const charts = chartPluginMock.createStartContract();
return {
- uiSettings,
- charts: {
- colors,
- },
+ uiSettings: mockUiSettings,
+ charts: charts,
};
};
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/chart.test.js
similarity index 77%
rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/chart.js
rename to src/plugins/vis_type_vislib/public/vislib/visualizations/chart.test.js
index 2b41ce5d1a5c6..94c9693819b69 100644
--- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/chart.js
+++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/chart.test.js
@@ -18,11 +18,10 @@
*/
import d3 from 'd3';
-import expect from '@kbn/expect';
-
-import { Chart } from '../../../../../../../plugins/vis_type_vislib/public/vislib/visualizations/_chart';
-import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks';
-import { getVis } from '../_vis_fixture';
+import { setHTMLElementClientSizes, setSVGElementGetBBox } from '../../../../../test_utils/public';
+import { Chart } from './_chart';
+import { getMockUiState } from '../../fixtures/mocks';
+import { getVis } from './_vis_fixture';
describe('Vislib _chart Test Suite', function () {
let vis;
@@ -106,6 +105,14 @@ describe('Vislib _chart Test Suite', function () {
yAxisLabel: 'Count',
};
+ let mockedHTMLElementClientSizes;
+ let mockedSVGElementGetBBox;
+
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ });
+
beforeEach(() => {
el = d3.select('body').append('div').attr('class', 'column-chart');
@@ -127,11 +134,16 @@ describe('Vislib _chart Test Suite', function () {
vis.destroy();
});
- it('should be a constructor for visualization modules', function () {
- expect(myChart instanceof Chart).to.be(true);
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ });
+
+ test('should be a constructor for visualization modules', function () {
+ expect(myChart instanceof Chart).toBe(true);
});
- it('should have a render method', function () {
- expect(typeof myChart.render === 'function').to.be(true);
+ test('should have a render method', function () {
+ expect(typeof myChart.render === 'function').toBe(true);
});
});
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/gauge_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/gauge_chart.test.js
similarity index 65%
rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/gauge_chart.js
rename to src/plugins/vis_type_vislib/public/vislib/visualizations/gauge_chart.test.js
index 7c588800ae659..6fdc2a134b820 100644
--- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/gauge_chart.js
+++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/gauge_chart.test.js
@@ -19,11 +19,11 @@
import $ from 'jquery';
import _ from 'lodash';
-import expect from '@kbn/expect';
+import { setHTMLElementClientSizes, setSVGElementGetBBox } from '../../../../../test_utils/public';
-import data from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/terms/_series_multiple';
-import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks';
-import { getVis } from '../_vis_fixture';
+import data from '../../fixtures/mock_data/terms/_series_multiple';
+import { getMockUiState } from '../../fixtures/mocks';
+import { getVis } from './_vis_fixture';
describe('Vislib Gauge Chart Test Suite', function () {
let vis;
@@ -82,6 +82,14 @@ describe('Vislib Gauge Chart Test Suite', function () {
chartEl = vis.handler.charts[0].chartEl;
}
+ let mockedHTMLElementClientSizes;
+ let mockedSVGElementGetBBox;
+
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ });
+
beforeEach(() => {
generateVis();
});
@@ -91,55 +99,60 @@ describe('Vislib Gauge Chart Test Suite', function () {
$('.visChart').remove();
});
- it('creates meter gauge', function () {
- expect($(chartEl).find('svg').length).to.equal(5);
- expect($(chartEl).find('svg > g > g > text').text()).to.equal('2820231918357341352');
+ afterAll(function () {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ });
+
+ test('creates meter gauge', function () {
+ expect($(chartEl).find('svg').length).toEqual(5);
+ expect($(chartEl).find('svg > g > g > text').text()).toEqual('2820231918357341352');
});
- it('creates circle gauge', function () {
+ test('creates circle gauge', function () {
generateVis({
gauge: {
minAngle: 0,
maxAngle: 2 * Math.PI,
},
});
- expect($(chartEl).find('svg').length).to.equal(5);
+ expect($(chartEl).find('svg').length).toEqual(5);
});
- it('creates gauge with automatic mode', function () {
+ test('creates gauge with automatic mode', function () {
generateVis({
gauge: {
alignment: 'automatic',
},
});
- expect($(chartEl).find('svg').width()).to.equal(197);
+ expect($(chartEl).find('svg')[0].getAttribute('width')).toEqual('97');
});
- it('creates gauge with vertical mode', function () {
+ test('creates gauge with vertical mode', function () {
generateVis({
gauge: {
alignment: 'vertical',
},
});
- expect($(chartEl).find('svg').width()).to.equal($(chartEl).width());
+ expect($(chartEl).find('svg').width()).toEqual($(chartEl).width());
});
- it('applies range settings correctly', function () {
+ test('applies range settings correctly', function () {
const paths = $(chartEl).find('svg > g > g:nth-child(1) > path:nth-child(2)');
const fills = [];
paths.each(function () {
fills.push(this.style.fill);
});
- expect(fills).to.eql([
- 'rgb(165, 0, 38)',
- 'rgb(255, 255, 190)',
- 'rgb(255, 255, 190)',
- 'rgb(0, 104, 55)',
- 'rgb(0, 104, 55)',
+ expect(fills).toEqual([
+ 'rgb(165,0,38)',
+ 'rgb(255,255,190)',
+ 'rgb(255,255,190)',
+ 'rgb(0,104,55)',
+ 'rgb(0,104,55)',
]);
});
- it('applies color schema correctly', function () {
+ test('applies color schema correctly', function () {
generateVis({
gauge: {
colorSchema: 'Blues',
@@ -150,12 +163,12 @@ describe('Vislib Gauge Chart Test Suite', function () {
paths.each(function () {
fills.push(this.style.fill);
});
- expect(fills).to.eql([
- 'rgb(8, 48, 107)',
- 'rgb(107, 174, 214)',
- 'rgb(107, 174, 214)',
- 'rgb(247, 251, 255)',
- 'rgb(247, 251, 255)',
+ expect(fills).toEqual([
+ 'rgb(8,48,107)',
+ 'rgb(107,174,214)',
+ 'rgb(107,174,214)',
+ 'rgb(247,251,255)',
+ 'rgb(247,251,255)',
]);
});
});
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/pie_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.test.js
similarity index 65%
rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/pie_chart.js
rename to src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.test.js
index d245905729c7e..e2da33d0808ba 100644
--- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/pie_chart.js
+++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart.test.js
@@ -20,16 +20,25 @@
import d3 from 'd3';
import _ from 'lodash';
import $ from 'jquery';
-import expect from '@kbn/expect';
-
-import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks';
-import { getVis } from '../_vis_fixture';
+import {
+ setHTMLElementClientSizes,
+ setSVGElementGetBBox,
+ setSVGElementGetComputedTextLength,
+} from '../../../../../test_utils/public';
+import { getMockUiState } from '../../fixtures/mocks';
+import { getVis } from './_vis_fixture';
import { pieChartMockData } from './pie_chart_mock_data';
const names = ['rows', 'columns', 'slices'];
const sizes = [0, 5, 15, 30, 60, 120];
+let mockedHTMLElementClientSizes = {};
+let mockWidth;
+let mockHeight;
+let mockedSVGElementGetBBox;
+let mockedSVGElementGetComputedTextLength;
+
describe('No global chart settings', function () {
const visLibParams1 = {
el: '
',
@@ -40,6 +49,14 @@ describe('No global chart settings', function () {
let chart1;
let mockUiState;
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100);
+ mockWidth = jest.spyOn($.prototype, 'width').mockReturnValue(120);
+ mockHeight = jest.spyOn($.prototype, 'height').mockReturnValue(120);
+ });
+
beforeEach(() => {
chart1 = getVis(visLibParams1);
mockUiState = getMockUiState();
@@ -53,8 +70,16 @@ describe('No global chart settings', function () {
chart1.destroy();
});
- it('should render chart titles for all charts', function () {
- expect($(chart1.element).find('.visAxis__splitTitles--y').length).to.be(1);
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ mockedSVGElementGetComputedTextLength.mockRestore();
+ mockWidth.mockRestore();
+ mockHeight.mockRestore();
+ });
+
+ test('should render chart titles for all charts', function () {
+ expect($(chart1.element).find('.visAxis__splitTitles--y').length).toBe(1);
});
describe('_validatePieData method', function () {
@@ -76,24 +101,54 @@ describe('No global chart settings', function () {
{ slices: { children: [{}] } },
];
- it('should throw an error when all charts contain zeros', function () {
+ test('should throw an error when all charts contain zeros', function () {
expect(function () {
chart1.handler.ChartClass.prototype._validatePieData(allZeros);
- }).to.throwError();
+ }).toThrowError();
});
- it('should not throw an error when only some or no charts contain zeros', function () {
+ test('should not throw an error when only some or no charts contain zeros', function () {
expect(function () {
chart1.handler.ChartClass.prototype._validatePieData(someZeros);
- }).to.not.throwError();
+ }).not.toThrowError();
expect(function () {
chart1.handler.ChartClass.prototype._validatePieData(noZeros);
- }).to.not.throwError();
+ }).not.toThrowError();
});
});
});
describe('Vislib PieChart Class Test Suite', function () {
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100);
+ let width = 120;
+ let height = 120;
+ const mockWidth = jest.spyOn($.prototype, 'width');
+ mockWidth.mockImplementation((size) => {
+ if (size === undefined) {
+ return width;
+ }
+ width = size;
+ });
+ const mockHeight = jest.spyOn($.prototype, 'height');
+ mockHeight.mockImplementation((size) => {
+ if (size === undefined) {
+ return height;
+ }
+ height = size;
+ });
+ });
+
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ mockedSVGElementGetComputedTextLength.mockRestore();
+ mockWidth.mockRestore();
+ mockHeight.mockRestore();
+ });
+
['rowData', 'columnData', 'sliceData'].forEach(function (aggItem, i) {
describe('Vislib PieChart Class Test Suite for ' + names[i] + ' data', function () {
const mockPieData = pieChartMockData[aggItem];
@@ -132,15 +187,15 @@ describe('Vislib PieChart Class Test Suite', function () {
});
});
- it('should attach a click event', function () {
+ test('should attach a click event', function () {
vis.handler.charts.forEach(function () {
- expect(onClick).to.be(true);
+ expect(onClick).toBe(true);
});
});
- it('should attach a hover event', function () {
+ test('should attach a hover event', function () {
vis.handler.charts.forEach(function () {
- expect(onMouseOver).to.be(true);
+ expect(onMouseOver).toBe(true);
});
});
});
@@ -151,25 +206,25 @@ describe('Vislib PieChart Class Test Suite', function () {
let svg;
let slices;
- it('should return an SVG object', function () {
+ test('should return an SVG object', function () {
vis.handler.charts.forEach(function (chart) {
$(chart.chartEl).find('svg').empty();
width = $(chart.chartEl).width();
height = $(chart.chartEl).height();
svg = d3.select($(chart.chartEl).find('svg')[0]);
slices = chart.chartData.slices;
- expect(_.isObject(chart.addPath(width, height, svg, slices))).to.be(true);
+ expect(_.isObject(chart.addPath(width, height, svg, slices))).toBe(true);
});
});
- it('should draw path elements', function () {
+ test('should draw path elements', function () {
vis.handler.charts.forEach(function (chart) {
// test whether path elements are drawn
- expect($(chart.chartEl).find('path').length).to.be.greaterThan(0);
+ expect($(chart.chartEl).find('path').length).toBeGreaterThan(0);
});
});
- it('should draw labels', function () {
+ test('should draw labels', function () {
vis.handler.charts.forEach(function (chart) {
$(chart.chartEl).find('svg').empty();
width = $(chart.chartEl).width();
@@ -178,22 +233,22 @@ describe('Vislib PieChart Class Test Suite', function () {
slices = chart.chartData.slices;
chart._attr.labels.show = true;
chart.addPath(width, height, svg, slices);
- expect($(chart.chartEl).find('text.label-text').length).to.be.greaterThan(0);
+ expect($(chart.chartEl).find('text.label-text').length).toBeGreaterThan(0);
});
});
});
describe('draw method', function () {
- it('should return a function', function () {
+ test('should return a function', function () {
vis.handler.charts.forEach(function (chart) {
- expect(_.isFunction(chart.draw())).to.be(true);
+ expect(_.isFunction(chart.draw())).toBe(true);
});
});
});
sizes.forEach(function (size) {
describe('containerTooSmall error', function () {
- it('should throw an error', function () {
+ test('should throw an error', function () {
// 20px is the minimum height and width
vis.handler.charts.forEach(function (chart) {
$(chart.chartEl).height(size);
@@ -202,12 +257,12 @@ describe('Vislib PieChart Class Test Suite', function () {
if (size < 20) {
expect(function () {
chart.render();
- }).to.throwError();
+ }).toThrowError();
}
});
});
- it('should not throw an error', function () {
+ test('should not throw an error', function () {
vis.handler.charts.forEach(function (chart) {
$(chart.chartEl).height(size);
$(chart.chartEl).width(size);
@@ -215,7 +270,7 @@ describe('Vislib PieChart Class Test Suite', function () {
if (size > 20) {
expect(function () {
chart.render();
- }).to.not.throwError();
+ }).not.toThrowError();
}
});
});
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/pie_chart_mock_data.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart_mock_data.js
similarity index 100%
rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/pie_chart_mock_data.js
rename to src/plugins/vis_type_vislib/public/vislib/visualizations/pie_chart_mock_data.js
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/area_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.test.js
similarity index 64%
rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/area_chart.js
rename to src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.test.js
index fd2240c0c64c5..3cd58060978ee 100644
--- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/area_chart.js
+++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/area_chart.test.js
@@ -20,18 +20,22 @@
import d3 from 'd3';
import _ from 'lodash';
import $ from 'jquery';
-import expect from '@kbn/expect';
+import {
+ setHTMLElementClientSizes,
+ setSVGElementGetBBox,
+ setSVGElementGetComputedTextLength,
+} from '../../../../../../test_utils/public';
-import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks';
+import { getMockUiState } from '../../../fixtures/mocks';
import { getVis } from '../_vis_fixture';
const dataTypesArray = {
- 'series pos': require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series'),
- 'series pos neg': require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_pos_neg'),
- 'series neg': require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_neg'),
- 'term columns': require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/terms/_columns'),
- 'range rows': require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/range/_rows'),
- stackedSeries: require('../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series'),
+ 'series pos': import('../../../fixtures/mock_data/date_histogram/_series'),
+ 'series pos neg': import('../../../fixtures/mock_data/date_histogram/_series_pos_neg'),
+ 'series neg': import('../../../fixtures/mock_data/date_histogram/_series_neg'),
+ 'term columns': import('../../../fixtures/mock_data/terms/_columns'),
+ 'range rows': import('../../../fixtures/mock_data/range/_rows'),
+ stackedSeries: import('../../../fixtures/mock_data/date_histogram/_stacked_series'),
};
const visLibParams = {
@@ -41,22 +45,38 @@ const visLibParams = {
mode: 'stacked',
};
+let mockedHTMLElementClientSizes;
+let mockedSVGElementGetBBox;
+let mockedSVGElementGetComputedTextLength;
+
_.forOwn(dataTypesArray, function (dataType, dataTypeName) {
describe('Vislib Area Chart Test Suite for ' + dataTypeName + ' Data', function () {
let vis;
let mockUiState;
- beforeEach(() => {
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100);
+ });
+
+ beforeEach(async () => {
vis = getVis(visLibParams);
mockUiState = getMockUiState();
vis.on('brush', _.noop);
- vis.render(dataType, mockUiState);
+ vis.render(await dataType, mockUiState);
});
afterEach(function () {
vis.destroy();
});
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ mockedSVGElementGetComputedTextLength.mockRestore();
+ });
+
describe('stackData method', function () {
let stackedData;
let isStacked;
@@ -73,15 +93,15 @@ _.forOwn(dataTypesArray, function (dataType, dataTypeName) {
});
});
- it('should append a d.y0 key to the data object', function () {
- expect(isStacked).to.be(true);
+ test('should append a d.y0 key to the data object', function () {
+ expect(isStacked).toBe(true);
});
});
describe('addPath method', function () {
- it('should append a area paths', function () {
+ test('should append a area paths', function () {
vis.handler.charts.forEach(function (chart) {
- expect($(chart.chartEl).find('path').length).to.be.greaterThan(0);
+ expect($(chart.chartEl).find('path').length).toBeGreaterThan(0);
});
});
});
@@ -101,9 +121,9 @@ _.forOwn(dataTypesArray, function (dataType, dataTypeName) {
});
});
- it('should attach a hover event', function () {
+ test('should attach a hover event', function () {
vis.handler.charts.forEach(function () {
- expect(onMouseOver).to.be(true);
+ expect(onMouseOver).toBe(true);
});
});
});
@@ -134,33 +154,33 @@ _.forOwn(dataTypesArray, function (dataType, dataTypeName) {
// listeners, however, I was not able to test for the listener
// function being present. I will need to update this test
// in the future.
- it('should attach a brush g element', function () {
+ test('should attach a brush g element', function () {
vis.handler.charts.forEach(function () {
- expect(onBrush).to.be(true);
+ expect(onBrush).toBe(true);
});
});
- it('should attach a click event', function () {
+ test('should attach a click event', function () {
vis.handler.charts.forEach(function () {
- expect(onClick).to.be(true);
+ expect(onClick).toBe(true);
});
});
- it('should attach a hover event', function () {
+ test('should attach a hover event', function () {
vis.handler.charts.forEach(function () {
- expect(onMouseOver).to.be(true);
+ expect(onMouseOver).toBe(true);
});
});
});
describe('addCircles method', function () {
- it('should append circles', function () {
+ test('should append circles', function () {
vis.handler.charts.forEach(function (chart) {
- expect($(chart.chartEl).find('circle').length).to.be.greaterThan(0);
+ expect($(chart.chartEl).find('circle').length).toBeGreaterThan(0);
});
});
- it('should not draw circles where d.y === 0', function () {
+ test('should not draw circles where d.y === 0', function () {
vis.handler.charts.forEach(function (chart) {
const series = chart.chartData.series;
const isZero = series.some(function (d) {
@@ -172,80 +192,80 @@ _.forOwn(dataTypesArray, function (dataType, dataTypeName) {
});
if (isZero) {
- expect(isNotDrawn).to.be(false);
+ expect(isNotDrawn).toBe(false);
}
});
});
});
describe('draw method', function () {
- it('should return a function', function () {
+ test('should return a function', function () {
vis.handler.charts.forEach(function (chart) {
- expect(_.isFunction(chart.draw())).to.be(true);
+ expect(_.isFunction(chart.draw())).toBe(true);
});
});
- it('should return a yMin and yMax', function () {
+ test('should return a yMin and yMax', function () {
vis.handler.charts.forEach(function (chart) {
const yAxis = chart.handler.valueAxes[0];
const domain = yAxis.getScale().domain();
- expect(domain[0]).to.not.be(undefined);
- expect(domain[1]).to.not.be(undefined);
+ expect(domain[0]).not.toBe(undefined);
+ expect(domain[1]).not.toBe(undefined);
});
});
- it('should render a zero axis line', function () {
+ test('should render a zero axis line', function () {
vis.handler.charts.forEach(function (chart) {
const yAxis = chart.handler.valueAxes[0];
if (yAxis.yMin < 0 && yAxis.yMax > 0) {
- expect($(chart.chartEl).find('line.zero-line').length).to.be(1);
+ expect($(chart.chartEl).find('line.zero-line').length).toBe(1);
}
});
});
});
describe('defaultYExtents is true', function () {
- beforeEach(function () {
+ beforeEach(async function () {
vis.visConfigArgs.defaultYExtents = true;
- vis.render(dataType, mockUiState);
+ vis.render(await dataType, mockUiState);
});
- it('should return yAxis extents equal to data extents', function () {
+ test('should return yAxis extents equal to data extents', function () {
vis.handler.charts.forEach(function (chart) {
const yAxis = chart.handler.valueAxes[0];
const min = vis.handler.valueAxes[0].axisScale.getYMin();
const max = vis.handler.valueAxes[0].axisScale.getYMax();
const domain = yAxis.getScale().domain();
- expect(domain[0]).to.equal(min);
- expect(domain[1]).to.equal(max);
+ expect(domain[0]).toEqual(min);
+ expect(domain[1]).toEqual(max);
});
});
});
[0, 2, 4, 8].forEach(function (boundsMarginValue) {
describe('defaultYExtents is true and boundsMargin is defined', function () {
- beforeEach(function () {
+ beforeEach(async function () {
vis.visConfigArgs.defaultYExtents = true;
vis.visConfigArgs.boundsMargin = boundsMarginValue;
- vis.render(dataType, mockUiState);
+ vis.render(await dataType, mockUiState);
});
- it('should return yAxis extents equal to data extents with boundsMargin', function () {
+ test('should return yAxis extents equal to data extents with boundsMargin', function () {
vis.handler.charts.forEach(function (chart) {
const yAxis = chart.handler.valueAxes[0];
const min = vis.handler.valueAxes[0].axisScale.getYMin();
const max = vis.handler.valueAxes[0].axisScale.getYMax();
const domain = yAxis.getScale().domain();
if (min < 0 && max < 0) {
- expect(domain[0]).to.equal(min);
- expect(domain[1] - boundsMarginValue).to.equal(max);
+ expect(domain[0]).toEqual(min);
+ expect(domain[1] - boundsMarginValue).toEqual(max);
} else if (min > 0 && max > 0) {
- expect(domain[0] + boundsMarginValue).to.equal(min);
- expect(domain[1]).to.equal(max);
+ expect(domain[0] + boundsMarginValue).toEqual(min);
+ expect(domain[1]).toEqual(max);
} else {
- expect(domain[0]).to.equal(min);
- expect(domain[1]).to.equal(max);
+ expect(domain[0]).toEqual(min);
+ expect(domain[1]).toEqual(max);
}
});
});
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/column_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.test.js
similarity index 59%
rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/column_chart.js
rename to src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.test.js
index 6b7ccaed25d49..f3d8d66df2d85 100644
--- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/column_chart.js
+++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/column_chart.test.js
@@ -20,20 +20,24 @@
import _ from 'lodash';
import d3 from 'd3';
import $ from 'jquery';
-import expect from '@kbn/expect';
+import {
+ setHTMLElementClientSizes,
+ setSVGElementGetBBox,
+ setSVGElementGetComputedTextLength,
+} from '../../../../../../test_utils/public';
// Data
-import series from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series';
-import seriesPosNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_pos_neg';
-import seriesNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_neg';
-import termsColumns from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/terms/_columns';
-import histogramRows from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_rows';
-import stackedSeries from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series';
-
-import { seriesMonthlyInterval } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_monthly_interval';
-import { rowsSeriesWithHoles } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows_series_with_holes';
-import rowsWithZeros from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_rows';
-import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks';
+import series from '../../../fixtures/mock_data/date_histogram/_series';
+import seriesPosNeg from '../../../fixtures/mock_data/date_histogram/_series_pos_neg';
+import seriesNeg from '../../../fixtures/mock_data/date_histogram/_series_neg';
+import termsColumns from '../../../fixtures/mock_data/terms/_columns';
+import histogramRows from '../../../fixtures/mock_data/histogram/_rows';
+import stackedSeries from '../../../fixtures/mock_data/date_histogram/_stacked_series';
+
+import { seriesMonthlyInterval } from '../../../fixtures/mock_data/date_histogram/_series_monthly_interval';
+import { rowsSeriesWithHoles } from '../../../fixtures/mock_data/date_histogram/_rows_series_with_holes';
+import rowsWithZeros from '../../../fixtures/mock_data/date_histogram/_rows';
+import { getMockUiState } from '../../../fixtures/mocks';
import { getVis } from '../_vis_fixture';
// tuple, with the format [description, mode, data]
@@ -46,6 +50,10 @@ const dataTypesArray = [
['stackedSeries', 'stacked', stackedSeries],
];
+let mockedHTMLElementClientSizes;
+let mockedSVGElementGetBBox;
+let mockedSVGElementGetComputedTextLength;
+
dataTypesArray.forEach(function (dataType) {
const name = dataType[0];
const mode = dataType[1];
@@ -66,6 +74,12 @@ dataTypesArray.forEach(function (dataType) {
},
};
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100);
+ });
+
beforeEach(() => {
vis = getVis(visLibParams);
mockUiState = getMockUiState();
@@ -77,6 +91,12 @@ dataTypesArray.forEach(function (dataType) {
vis.destroy();
});
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ mockedSVGElementGetComputedTextLength.mockRestore();
+ });
+
describe('stackData method', function () {
let stackedData;
let isStacked;
@@ -93,21 +113,21 @@ dataTypesArray.forEach(function (dataType) {
});
});
- it('should stack values when mode is stacked', function () {
+ test('should stack values when mode is stacked', function () {
if (mode === 'stacked') {
- expect(isStacked).to.be(true);
+ expect(isStacked).toBe(true);
}
});
- it('should stack values when mode is percentage', function () {
+ test('should stack values when mode is percentage', function () {
if (mode === 'percentage') {
- expect(isStacked).to.be(true);
+ expect(isStacked).toBe(true);
}
});
});
describe('addBars method', function () {
- it('should append rects', function () {
+ test('should append rects', function () {
let numOfSeries;
let numOfValues;
let product;
@@ -116,7 +136,7 @@ dataTypesArray.forEach(function (dataType) {
numOfSeries = chart.chartData.series.length;
numOfValues = chart.chartData.series[0].values.length;
product = numOfSeries * numOfValues;
- expect($(chart.chartEl).find('.series rect')).to.have.length(product);
+ expect($(chart.chartEl).find('.series rect')).toHaveLength(product);
});
});
});
@@ -138,53 +158,53 @@ dataTypesArray.forEach(function (dataType) {
};
}
- it('should attach the brush if data is a set is ordered', function () {
+ test('should attach the brush if data is a set is ordered', function () {
vis.handler.charts.forEach(function (chart) {
const has = checkChart(chart);
const ordered = vis.handler.data.get('ordered');
const allowBrushing = Boolean(ordered);
- expect(has.brush).to.be(allowBrushing);
+ expect(has.brush).toBe(allowBrushing);
});
});
- it('should attach a click event', function () {
+ test('should attach a click event', function () {
vis.handler.charts.forEach(function (chart) {
const has = checkChart(chart);
- expect(has.click).to.be(true);
+ expect(has.click).toBe(true);
});
});
- it('should attach a hover event', function () {
+ test('should attach a hover event', function () {
vis.handler.charts.forEach(function (chart) {
const has = checkChart(chart);
- expect(has.mouseOver).to.be(true);
+ expect(has.mouseOver).toBe(true);
});
});
});
describe('draw method', function () {
- it('should return a function', function () {
+ test('should return a function', function () {
vis.handler.charts.forEach(function (chart) {
- expect(_.isFunction(chart.draw())).to.be(true);
+ expect(_.isFunction(chart.draw())).toBe(true);
});
});
- it('should return a yMin and yMax', function () {
+ test('should return a yMin and yMax', function () {
vis.handler.charts.forEach(function (chart) {
const yAxis = chart.handler.valueAxes[0];
const domain = yAxis.getScale().domain();
- expect(domain[0]).to.not.be(undefined);
- expect(domain[1]).to.not.be(undefined);
+ expect(domain[0]).not.toBe(undefined);
+ expect(domain[1]).not.toBe(undefined);
});
});
- it('should render a zero axis line', function () {
+ test('should render a zero axis line', function () {
vis.handler.charts.forEach(function (chart) {
const yAxis = chart.handler.valueAxes[0];
if (yAxis.yMin < 0 && yAxis.yMax > 0) {
- expect($(chart.chartEl).find('line.zero-line').length).to.be(1);
+ expect($(chart.chartEl).find('line.zero-line').length).toBe(1);
}
});
});
@@ -196,14 +216,14 @@ dataTypesArray.forEach(function (dataType) {
vis.render(data, mockUiState);
});
- it('should return yAxis extents equal to data extents', function () {
+ test('should return yAxis extents equal to data extents', function () {
vis.handler.charts.forEach(function (chart) {
const yAxis = chart.handler.valueAxes[0];
const min = vis.handler.valueAxes[0].axisScale.getYMin();
const max = vis.handler.valueAxes[0].axisScale.getYMax();
const domain = yAxis.getScale().domain();
- expect(domain[0]).to.equal(min);
- expect(domain[1]).to.equal(max);
+ expect(domain[0]).toEqual(min);
+ expect(domain[1]).toEqual(max);
});
});
});
@@ -215,21 +235,21 @@ dataTypesArray.forEach(function (dataType) {
vis.render(data, mockUiState);
});
- it('should return yAxis extents equal to data extents with boundsMargin', function () {
+ test('should return yAxis extents equal to data extents with boundsMargin', function () {
vis.handler.charts.forEach(function (chart) {
const yAxis = chart.handler.valueAxes[0];
const min = vis.handler.valueAxes[0].axisScale.getYMin();
const max = vis.handler.valueAxes[0].axisScale.getYMax();
const domain = yAxis.getScale().domain();
if (min < 0 && max < 0) {
- expect(domain[0]).to.equal(min);
- expect(domain[1] - boundsMarginValue).to.equal(max);
+ expect(domain[0]).toEqual(min);
+ expect(domain[1] - boundsMarginValue).toEqual(max);
} else if (min > 0 && max > 0) {
- expect(domain[0] + boundsMarginValue).to.equal(min);
- expect(domain[1]).to.equal(max);
+ expect(domain[0] + boundsMarginValue).toEqual(min);
+ expect(domain[1]).toEqual(max);
} else {
- expect(domain[0]).to.equal(min);
- expect(domain[1]).to.equal(max);
+ expect(domain[0]).toEqual(min);
+ expect(domain[1]).toEqual(max);
}
});
});
@@ -249,6 +269,12 @@ describe('stackData method - data set with zeros in percentage mode', function (
zeroFill: true,
};
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100);
+ });
+
beforeEach(() => {
vis = getVis(visLibParams);
mockUiState = getMockUiState();
@@ -259,29 +285,35 @@ describe('stackData method - data set with zeros in percentage mode', function (
vis.destroy();
});
- it('should not mutate the injected zeros', function () {
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ mockedSVGElementGetComputedTextLength.mockRestore();
+ });
+
+ test('should not mutate the injected zeros', function () {
vis.render(seriesMonthlyInterval, mockUiState);
- expect(vis.handler.charts).to.have.length(1);
+ expect(vis.handler.charts).toHaveLength(1);
const chart = vis.handler.charts[0];
- expect(chart.chartData.series).to.have.length(1);
+ expect(chart.chartData.series).toHaveLength(1);
const series = chart.chartData.series[0].values;
// with the interval set in seriesMonthlyInterval data, the point at x=1454309600000 does not exist
const point = _.find(series, ['x', 1454309600000]);
- expect(point).to.not.be(undefined);
- expect(point.y).to.be(0);
+ expect(point).not.toBe(undefined);
+ expect(point.y).toBe(0);
});
- it('should not mutate zeros that exist in the data', function () {
+ test('should not mutate zeros that exist in the data', function () {
vis.render(rowsWithZeros, mockUiState);
- expect(vis.handler.charts).to.have.length(2);
+ expect(vis.handler.charts).toHaveLength(2);
const chart = vis.handler.charts[0];
- expect(chart.chartData.series).to.have.length(5);
+ expect(chart.chartData.series).toHaveLength(5);
const series = chart.chartData.series[0].values;
const point = _.find(series, ['x', 1415826240000]);
- expect(point).to.not.be(undefined);
- expect(point.y).to.be(0);
+ expect(point).not.toBe(undefined);
+ expect(point.y).toBe(0);
});
});
@@ -296,6 +328,12 @@ describe('datumWidth - split chart data set with holes', function () {
zeroFill: true,
};
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100);
+ });
+
beforeEach(() => {
vis = getVis(visLibParams);
mockUiState = getMockUiState();
@@ -307,14 +345,20 @@ describe('datumWidth - split chart data set with holes', function () {
vis.destroy();
});
- it('should not have bar widths that span multiple time bins', function () {
- expect(vis.handler.charts.length).to.equal(1);
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ mockedSVGElementGetComputedTextLength.mockRestore();
+ });
+
+ test('should not have bar widths that span multiple time bins', function () {
+ expect(vis.handler.charts.length).toEqual(1);
const chart = vis.handler.charts[0];
const rects = $(chart.chartEl).find('.series rect');
const MAX_WIDTH_IN_PIXELS = 27;
rects.each(function () {
- const width = $(this).attr('width');
- expect(width).to.be.lessThan(MAX_WIDTH_IN_PIXELS);
+ const width = parseInt($(this).attr('width'), 10);
+ expect(width).toBeLessThan(MAX_WIDTH_IN_PIXELS);
});
});
});
@@ -330,6 +374,15 @@ describe('datumWidth - monthly interval', function () {
zeroFill: true,
};
+ let mockWidth;
+
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100);
+ mockWidth = jest.spyOn($.prototype, 'width').mockReturnValue(900);
+ });
+
beforeEach(() => {
vis = getVis(visLibParams);
mockUiState = getMockUiState();
@@ -341,12 +394,19 @@ describe('datumWidth - monthly interval', function () {
vis.destroy();
});
- it('should vary bar width when date histogram intervals are not equal', function () {
- expect(vis.handler.charts.length).to.equal(1);
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ mockedSVGElementGetComputedTextLength.mockRestore();
+ mockWidth.mockRestore();
+ });
+
+ test('should vary bar width when date histogram intervals are not equal', function () {
+ expect(vis.handler.charts.length).toEqual(1);
const chart = vis.handler.charts[0];
const rects = $(chart.chartEl).find('.series rect');
- const januaryBarWidth = $(rects.get(0)).attr('width');
- const februaryBarWidth = $(rects.get(1)).attr('width');
- expect(februaryBarWidth).to.be.lessThan(januaryBarWidth);
+ const januaryBarWidth = parseInt($(rects.get(0)).attr('width'), 10);
+ const februaryBarWidth = parseInt($(rects.get(1)).attr('width'), 10);
+ expect(februaryBarWidth).toBeLessThan(januaryBarWidth);
});
});
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/heatmap_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.test.js
similarity index 65%
rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/heatmap_chart.js
rename to src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.test.js
index 9fa51fb59ed48..8c727d225c6c3 100644
--- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/heatmap_chart.js
+++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/heatmap_chart.test.js
@@ -20,15 +20,19 @@
import _ from 'lodash';
import $ from 'jquery';
import d3 from 'd3';
-import expect from '@kbn/expect';
+import {
+ setHTMLElementClientSizes,
+ setSVGElementGetBBox,
+ setSVGElementGetComputedTextLength,
+} from '../../../../../../test_utils/public';
// Data
-import series from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series';
-import seriesPosNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_pos_neg';
-import seriesNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_neg';
-import termsColumns from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/terms/_columns';
-import stackedSeries from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_stacked_series';
-import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks';
+import series from '../../../fixtures/mock_data/date_histogram/_series';
+import seriesPosNeg from '../../../fixtures/mock_data/date_histogram/_series_pos_neg';
+import seriesNeg from '../../../fixtures/mock_data/date_histogram/_series_neg';
+import termsColumns from '../../../fixtures/mock_data/terms/_columns';
+import stackedSeries from '../../../fixtures/mock_data/date_histogram/_stacked_series';
+import { getMockUiState } from '../../../fixtures/mocks';
import { getVis } from '../_vis_fixture';
// tuple, with the format [description, mode, data]
@@ -40,7 +44,26 @@ const dataTypesArray = [
['stackedSeries', stackedSeries],
];
+let mockedHTMLElementClientSizes;
+let mockedSVGElementGetBBox;
+let mockedSVGElementGetComputedTextLength;
+let mockWidth;
+
describe('Vislib Heatmap Chart Test Suite', function () {
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100);
+ mockWidth = jest.spyOn($.prototype, 'width').mockReturnValue(900);
+ });
+
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ mockedSVGElementGetComputedTextLength.mockRestore();
+ mockWidth.mockRestore();
+ });
+
dataTypesArray.forEach(function (dataType) {
const name = dataType[0];
const data = dataType[1];
@@ -76,7 +99,7 @@ describe('Vislib Heatmap Chart Test Suite', function () {
vis.destroy();
});
- it('category axes should be rendered in reverse order', () => {
+ test('category axes should be rendered in reverse order', () => {
const renderedCategoryAxes = vis.handler.renderArray.filter((item) => {
return (
item.constructor &&
@@ -84,22 +107,22 @@ describe('Vislib Heatmap Chart Test Suite', function () {
item.axisConfig.get('type') === 'category'
);
});
- expect(vis.handler.categoryAxes.length).to.equal(renderedCategoryAxes.length);
- expect(vis.handler.categoryAxes[0].axisConfig.get('id')).to.equal(
+ expect(vis.handler.categoryAxes.length).toEqual(renderedCategoryAxes.length);
+ expect(vis.handler.categoryAxes[0].axisConfig.get('id')).toEqual(
renderedCategoryAxes[1].axisConfig.get('id')
);
- expect(vis.handler.categoryAxes[1].axisConfig.get('id')).to.equal(
+ expect(vis.handler.categoryAxes[1].axisConfig.get('id')).toEqual(
renderedCategoryAxes[0].axisConfig.get('id')
);
});
describe('addSquares method', function () {
- it('should append rects', function () {
+ test('should append rects', function () {
vis.handler.charts.forEach(function (chart) {
const numOfRects = chart.chartData.series.reduce((result, series) => {
return result + series.values.length;
}, 0);
- expect($(chart.chartEl).find('.series rect')).to.have.length(numOfRects);
+ expect($(chart.chartEl).find('.series rect')).toHaveLength(numOfRects);
});
});
});
@@ -120,53 +143,53 @@ describe('Vislib Heatmap Chart Test Suite', function () {
};
}
- it('should attach the brush if data is a set of ordered dates', function () {
+ test('should attach the brush if data is a set of ordered dates', function () {
vis.handler.charts.forEach(function (chart) {
const has = checkChart(chart);
const ordered = vis.handler.data.get('ordered');
const date = Boolean(ordered && ordered.date);
- expect(has.brush).to.be(date);
+ expect(has.brush).toBe(date);
});
});
- it('should attach a click event', function () {
+ test('should attach a click event', function () {
vis.handler.charts.forEach(function (chart) {
const has = checkChart(chart);
- expect(has.click).to.be(true);
+ expect(has.click).toBe(true);
});
});
- it('should attach a hover event', function () {
+ test('should attach a hover event', function () {
vis.handler.charts.forEach(function (chart) {
const has = checkChart(chart);
- expect(has.mouseOver).to.be(true);
+ expect(has.mouseOver).toBe(true);
});
});
});
describe('draw method', function () {
- it('should return a function', function () {
+ test('should return a function', function () {
vis.handler.charts.forEach(function (chart) {
- expect(_.isFunction(chart.draw())).to.be(true);
+ expect(_.isFunction(chart.draw())).toBe(true);
});
});
- it('should return a yMin and yMax', function () {
+ test('should return a yMin and yMax', function () {
vis.handler.charts.forEach(function (chart) {
const yAxis = chart.handler.valueAxes[0];
const domain = yAxis.getScale().domain();
- expect(domain[0]).to.not.be(undefined);
- expect(domain[1]).to.not.be(undefined);
+ expect(domain[0]).not.toBe(undefined);
+ expect(domain[1]).not.toBe(undefined);
});
});
});
- it('should define default colors', function () {
- expect(mockUiState.get('vis.defaultColors')).to.not.be(undefined);
+ test('should define default colors', function () {
+ expect(mockUiState.get('vis.defaultColors')).not.toBe(undefined);
});
- it('should set custom range', function () {
+ test('should set custom range', function () {
vis.destroy();
generateVis({
setColorRange: true,
@@ -178,14 +201,14 @@ describe('Vislib Heatmap Chart Test Suite', function () {
],
});
const labels = vis.getLegendLabels();
- expect(labels[0]).to.be('0 - 200');
- expect(labels[1]).to.be('200 - 400');
- expect(labels[2]).to.be('400 - 500');
- expect(labels[3]).to.be('500 - Infinity');
+ expect(labels[0]).toBe('0 - 200');
+ expect(labels[1]).toBe('200 - 400');
+ expect(labels[2]).toBe('400 - 500');
+ expect(labels[3]).toBe('500 - Infinity');
});
- it('should show correct Y axis title', function () {
- expect(vis.handler.categoryAxes[1].axisConfig.get('title.text')).to.equal('');
+ test('should show correct Y axis title', function () {
+ expect(vis.handler.categoryAxes[1].axisConfig.get('title.text')).toEqual('');
});
});
});
diff --git a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/line_chart.js b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.test.js
similarity index 67%
rename from src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/line_chart.js
rename to src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.test.js
index dae92c831cd8d..a84c74c095051 100644
--- a/src/legacy/core_plugins/kibana/public/__tests__/vis_type_vislib/visualizations/line_chart.js
+++ b/src/plugins/vis_type_vislib/public/vislib/visualizations/point_series/line_chart.test.js
@@ -18,18 +18,22 @@
*/
import d3 from 'd3';
-import expect from '@kbn/expect';
import $ from 'jquery';
import _ from 'lodash';
+import {
+ setHTMLElementClientSizes,
+ setSVGElementGetBBox,
+ setSVGElementGetComputedTextLength,
+} from '../../../../../../test_utils/public';
// Data
-import seriesPos from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series';
-import seriesPosNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_pos_neg';
-import seriesNeg from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/date_histogram/_series_neg';
-import histogramColumns from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/histogram/_columns';
-import rangeRows from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/range/_rows';
-import termSeries from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mock_data/terms/_series';
-import { getMockUiState } from '../../../../../../../plugins/vis_type_vislib/public/fixtures/mocks';
+import seriesPos from '../../../fixtures/mock_data/date_histogram/_series';
+import seriesPosNeg from '../../../fixtures/mock_data/date_histogram/_series_pos_neg';
+import seriesNeg from '../../../fixtures/mock_data/date_histogram/_series_neg';
+import histogramColumns from '../../../fixtures/mock_data/histogram/_columns';
+import rangeRows from '../../../fixtures/mock_data/range/_rows';
+import termSeries from '../../../fixtures/mock_data/terms/_series';
+import { getMockUiState } from '../../../fixtures/mocks';
import { getVis } from '../_vis_fixture';
const dataTypes = [
@@ -41,7 +45,23 @@ const dataTypes = [
['term series', termSeries],
];
+let mockedHTMLElementClientSizes;
+let mockedSVGElementGetBBox;
+let mockedSVGElementGetComputedTextLength;
+
describe('Vislib Line Chart', function () {
+ beforeAll(() => {
+ mockedHTMLElementClientSizes = setHTMLElementClientSizes(512, 512);
+ mockedSVGElementGetBBox = setSVGElementGetBBox(100);
+ mockedSVGElementGetComputedTextLength = setSVGElementGetComputedTextLength(100);
+ });
+
+ afterAll(() => {
+ mockedHTMLElementClientSizes.mockRestore();
+ mockedSVGElementGetBBox.mockRestore();
+ mockedSVGElementGetComputedTextLength.mockRestore();
+ });
+
dataTypes.forEach(function (type) {
const name = type[0];
const data = type[1];
@@ -94,37 +114,37 @@ describe('Vislib Line Chart', function () {
// listeners, however, I was not able to test for the listener
// function being present. I will need to update this test
// in the future.
- it('should attach a brush g element', function () {
+ test('should attach a brush g element', function () {
vis.handler.charts.forEach(function () {
- expect(onBrush).to.be(true);
+ expect(onBrush).toBe(true);
});
});
- it('should attach a click event', function () {
+ test('should attach a click event', function () {
vis.handler.charts.forEach(function () {
- expect(onClick).to.be(true);
+ expect(onClick).toBe(true);
});
});
- it('should attach a hover event', function () {
+ test('should attach a hover event', function () {
vis.handler.charts.forEach(function () {
- expect(onMouseOver).to.be(true);
+ expect(onMouseOver).toBe(true);
});
});
});
describe('addCircles method', function () {
- it('should append circles', function () {
+ test('should append circles', function () {
vis.handler.charts.forEach(function (chart) {
- expect($(chart.chartEl).find('circle').length).to.be.greaterThan(0);
+ expect($(chart.chartEl).find('circle').length).toBeGreaterThan(0);
});
});
});
describe('addLines method', function () {
- it('should append a paths', function () {
+ test('should append a paths', function () {
vis.handler.charts.forEach(function (chart) {
- expect($(chart.chartEl).find('path').length).to.be.greaterThan(0);
+ expect($(chart.chartEl).find('path').length).toBeGreaterThan(0);
});
});
});
@@ -132,7 +152,7 @@ describe('Vislib Line Chart', function () {
// Cannot seem to get these tests to work on the box
// They however pass in the browsers
//describe('addClipPath method', function () {
- // it('should append a clipPath', function () {
+ // test('should append a clipPath', function () {
// vis.handler.charts.forEach(function (chart) {
// expect($(chart.chartEl).find('clipPath').length).to.be(1);
// });
@@ -140,27 +160,27 @@ describe('Vislib Line Chart', function () {
//});
describe('draw method', function () {
- it('should return a function', function () {
+ test('should return a function', function () {
vis.handler.charts.forEach(function (chart) {
- expect(chart.draw()).to.be.a(Function);
+ expect(chart.draw()).toBeInstanceOf(Function);
});
});
- it('should return a yMin and yMax', function () {
+ test('should return a yMin and yMax', function () {
vis.handler.charts.forEach(function (chart) {
const yAxis = chart.handler.valueAxes[0];
const domain = yAxis.getScale().domain();
- expect(domain[0]).to.not.be(undefined);
- expect(domain[1]).to.not.be(undefined);
+ expect(domain[0]).not.toBe(undefined);
+ expect(domain[1]).not.toBe(undefined);
});
});
- it('should render a zero axis line', function () {
+ test('should render a zero axis line', function () {
vis.handler.charts.forEach(function (chart) {
const yAxis = chart.handler.valueAxes[0];
if (yAxis.yMin < 0 && yAxis.yMax > 0) {
- expect($(chart.chartEl).find('line.zero-line').length).to.be(1);
+ expect($(chart.chartEl).find('line.zero-line').length).toBe(1);
}
});
});
@@ -172,14 +192,14 @@ describe('Vislib Line Chart', function () {
vis.render(data, mockUiState);
});
- it('should return yAxis extents equal to data extents', function () {
+ test('should return yAxis extents equal to data extents', function () {
vis.handler.charts.forEach(function (chart) {
const yAxis = chart.handler.valueAxes[0];
const min = vis.handler.valueAxes[0].axisScale.getYMin();
const max = vis.handler.valueAxes[0].axisScale.getYMax();
const domain = yAxis.getScale().domain();
- expect(domain[0]).to.equal(min);
- expect(domain[1]).to.equal(max);
+ expect(domain[0]).toEqual(min);
+ expect(domain[1]).toEqual(max);
});
});
});
@@ -191,21 +211,21 @@ describe('Vislib Line Chart', function () {
vis.render(data, mockUiState);
});
- it('should return yAxis extents equal to data extents with boundsMargin', function () {
+ test('should return yAxis extents equal to data extents with boundsMargin', function () {
vis.handler.charts.forEach(function (chart) {
const yAxis = chart.handler.valueAxes[0];
const min = vis.handler.valueAxes[0].axisScale.getYMin();
const max = vis.handler.valueAxes[0].axisScale.getYMax();
const domain = yAxis.getScale().domain();
if (min < 0 && max < 0) {
- expect(domain[0]).to.equal(min);
- expect(domain[1] - boundsMarginValue).to.equal(max);
+ expect(domain[0]).toEqual(min);
+ expect(domain[1] - boundsMarginValue).toEqual(max);
} else if (min > 0 && max > 0) {
- expect(domain[0] + boundsMarginValue).to.equal(min);
- expect(domain[1]).to.equal(max);
+ expect(domain[0] + boundsMarginValue).toEqual(min);
+ expect(domain[1]).toEqual(max);
} else {
- expect(domain[0]).to.equal(min);
- expect(domain[1]).to.equal(max);
+ expect(domain[0]).toEqual(min);
+ expect(domain[1]).toEqual(max);
}
});
});
diff --git a/src/plugins/visualizations/kibana.json b/src/plugins/visualizations/kibana.json
index f3f9cbd8341ec..da3edfbdd3bf5 100644
--- a/src/plugins/visualizations/kibana.json
+++ b/src/plugins/visualizations/kibana.json
@@ -3,5 +3,6 @@
"version": "kibana",
"server": true,
"ui": true,
- "requiredPlugins": ["data", "expressions", "uiActions", "embeddable", "usageCollection", "inspector"]
+ "requiredPlugins": ["data", "expressions", "uiActions", "embeddable", "usageCollection", "inspector"],
+ "requiredBundles": ["kibanaUtils", "discover", "savedObjects"]
}
diff --git a/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap b/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap
index 53ef164685a1c..5458c88974572 100644
--- a/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap
+++ b/src/plugins/visualizations/public/wizard/__snapshots__/new_vis_modal.test.tsx.snap
@@ -139,7 +139,7 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
2 types found
-
+
+
+
+
+
+
-
-
+
+
+
-
+
+
+
+
+
+
+
@@ -562,121 +567,126 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
>
2 types found
-
+
+
+
+
+
+
-
+
+
+
-
+
+
+
+
+
+
+
@@ -813,121 +823,126 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
>
2 types found
-
+
+
+
+
+
+
-
+
+
+
-
+
+
+
+
+
+
+
@@ -1201,261 +1216,272 @@ exports[`NewVisModal filter for visualization types should render as expected 1`
className="visNewVisDialog__types"
data-test-subj="visNewDialogTypes"
>
-
-
- Vis with alias Url
-
- }
- onBlur={[Function]}
- onClick={[Function]}
- onFocus={[Function]}
- onMouseEnter={[Function]}
- onMouseLeave={[Function]}
- role="menuitem"
+
-
+ Vis with alias Url
+
+ }
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="menuitem"
- type="button"
>
-
-
-
-
-
-
-
-
+ type="popout"
+ >
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
- Vis with alias Url
-
-
-
-
-
-
- Vis with search
-
- }
- onBlur={[Function]}
- onClick={[Function]}
- onFocus={[Function]}
- onMouseEnter={[Function]}
- onMouseLeave={[Function]}
- role="menuitem"
+
+ Vis with alias Url
+
+
+
+
+
+
+
-
+ Vis with search
+
+ }
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="menuitem"
- type="button"
>
-
-
-
-
+
+
-
-
-
-
-
+
+
+
+
+
- Vis with search
-
-
-
-
-
-
- Vis Type 1
-
- }
- onBlur={[Function]}
- onClick={[Function]}
- onFocus={[Function]}
- onMouseEnter={[Function]}
- onMouseLeave={[Function]}
- role="menuitem"
+
+ Vis with search
+
+
+
+
+
+
+
-
+ Vis Type 1
+
+ }
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="menuitem"
- type="button"
>
-
-
-
-
+
+
-
-
-
-
-
+
+
+
+
+
- Vis Type 1
-
-
-
-
-
-
+
+ Vis Type 1
+
+
+
+
+
+
+
@@ -1683,7 +1709,7 @@ exports[`NewVisModal should render as expected 1`] = `
-
+
+
+
+
+
+
-
+
+
+
-
+
+
+
+
+
+
+
@@ -2073,120 +2104,125 @@ exports[`NewVisModal should render as expected 1`] = `
aria-live="polite"
class="euiScreenReaderOnly"
/>
-
+
+
+
+
+
+
-
+
+
+
-
+
+
+
+
+
+
+
@@ -2307,120 +2343,125 @@ exports[`NewVisModal should render as expected 1`] = `
aria-live="polite"
class="euiScreenReaderOnly"
/>
-
+
+
+
+
+
+
-
+
+
+
-
+
+
+
+
+
+
+
@@ -2643,261 +2684,272 @@ exports[`NewVisModal should render as expected 1`] = `
className="visNewVisDialog__types"
data-test-subj="visNewDialogTypes"
>
-
-
- Vis Type 1
-
- }
- onBlur={[Function]}
- onClick={[Function]}
- onFocus={[Function]}
- onMouseEnter={[Function]}
- onMouseLeave={[Function]}
- role="menuitem"
+
-
+ Vis Type 1
+
+ }
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="menuitem"
- type="button"
>
-
-
-
-
+
+
-
-
-
-
-
+
+
+
+
+
- Vis Type 1
-
-
-
-
-
-
- Vis with alias Url
-
- }
- onBlur={[Function]}
- onClick={[Function]}
- onFocus={[Function]}
- onMouseEnter={[Function]}
- onMouseLeave={[Function]}
- role="menuitem"
+
+ Vis Type 1
+
+
+
+
+
+
+
-
+ Vis with alias Url
+
+ }
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="menuitem"
- type="button"
>
-
-
-
-
-
-
-
-
+ type="popout"
+ >
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
- Vis with alias Url
-
-
-
-
-
-
- Vis with search
-
- }
- onBlur={[Function]}
- onClick={[Function]}
- onFocus={[Function]}
- onMouseEnter={[Function]}
- onMouseLeave={[Function]}
- role="menuitem"
+
+ Vis with alias Url
+
+
+
+
+
+
+
-
+ Vis with search
+
+ }
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="menuitem"
- type="button"
>
-
-
-
-
+
+
-
-
-
-
-
+
+
+
+
+
- Vis with search
-
-
-
-
-
-
+
+ Vis with search
+
+
+
+
+
+
+
diff --git a/src/plugins/visualize/kibana.json b/src/plugins/visualize/kibana.json
index c27cfec24b332..520d1e1daa6fe 100644
--- a/src/plugins/visualize/kibana.json
+++ b/src/plugins/visualize/kibana.json
@@ -11,5 +11,11 @@
"visualizations",
"embeddable"
],
- "optionalPlugins": ["home", "share"]
+ "optionalPlugins": ["home", "share"],
+ "requiredBundles": [
+ "kibanaUtils",
+ "kibanaReact",
+ "home",
+ "discover"
+ ]
}
diff --git a/src/test_utils/public/helpers/index.ts b/src/test_utils/public/helpers/index.ts
index c8447743ee287..fcc0102c76683 100644
--- a/src/test_utils/public/helpers/index.ts
+++ b/src/test_utils/public/helpers/index.ts
@@ -25,4 +25,9 @@ export { WithMemoryRouter, WithRoute, reactRouterMock } from './router_helpers';
export * from './utils';
-export { setSVGElementGetBBox, setHTMLElementOffset } from './jsdom_svg_mocks';
+export {
+ setSVGElementGetBBox,
+ setHTMLElementOffset,
+ setHTMLElementClientSizes,
+ setSVGElementGetComputedTextLength,
+} from './jsdom_svg_mocks';
diff --git a/src/test_utils/public/helpers/jsdom_svg_mocks.ts b/src/test_utils/public/helpers/jsdom_svg_mocks.ts
index dbc8266f663f1..6ef4204baa2ff 100644
--- a/src/test_utils/public/helpers/jsdom_svg_mocks.ts
+++ b/src/test_utils/public/helpers/jsdom_svg_mocks.ts
@@ -17,6 +17,20 @@
* under the License.
*/
+export const setHTMLElementClientSizes = (width: number, height: number) => {
+ const spyWidth = jest.spyOn(window.HTMLElement.prototype, 'clientWidth', 'get');
+ spyWidth.mockReturnValue(width);
+ const spyHeight = jest.spyOn(window.HTMLElement.prototype, 'clientHeight', 'get');
+ spyHeight.mockReturnValue(height);
+
+ return {
+ mockRestore: () => {
+ spyWidth.mockRestore();
+ spyHeight.mockRestore();
+ },
+ };
+};
+
export const setSVGElementGetBBox = (
width: number,
height: number,
@@ -41,6 +55,20 @@ export const setSVGElementGetBBox = (
};
};
+export const setSVGElementGetComputedTextLength = (width: number) => {
+ const SVGElementPrototype = SVGElement.prototype as any;
+ const originalGetComputedTextLength = SVGElementPrototype.getComputedTextLength;
+
+ // getComputedTextLength is not in the SVGElement.prototype object by default, so we cannot use jest.spyOn for that case
+ SVGElementPrototype.getComputedTextLength = jest.fn(() => width);
+
+ return {
+ mockRestore: () => {
+ SVGElementPrototype.getComputedTextLength = originalGetComputedTextLength;
+ },
+ };
+};
+
export const setHTMLElementOffset = (width: number, height: number) => {
const offsetWidthSpy = jest.spyOn(window.HTMLElement.prototype, 'offsetWidth', 'get');
offsetWidthSpy.mockReturnValue(width);
diff --git a/src/test_utils/public/index.ts b/src/test_utils/public/index.ts
index 4f46dfe1578db..e57f1ae8ea7a9 100644
--- a/src/test_utils/public/index.ts
+++ b/src/test_utils/public/index.ts
@@ -17,4 +17,9 @@
* under the License.
*/
-export { setSVGElementGetBBox, setHTMLElementOffset } from './helpers';
+export {
+ setSVGElementGetBBox,
+ setHTMLElementOffset,
+ setHTMLElementClientSizes,
+ setSVGElementGetComputedTextLength,
+} from './helpers';
diff --git a/tasks/config/karma.js b/tasks/config/karma.js
index 7c4f75bea8801..fa4bdc8ed2266 100644
--- a/tasks/config/karma.js
+++ b/tasks/config/karma.js
@@ -110,7 +110,7 @@ module.exports = function (grunt) {
customLaunchers: {
Chrome_Headless: {
base: 'Chrome',
- flags: ['--headless', '--disable-gpu', '--remote-debugging-port=9222', '--no-sandbox'],
+ flags: ['--headless', '--disable-gpu', '--remote-debugging-port=9222'],
},
},
diff --git a/tasks/test_jest.js b/tasks/test_jest.js
index 810ed42324840..d8f51806e8ddc 100644
--- a/tasks/test_jest.js
+++ b/tasks/test_jest.js
@@ -22,7 +22,7 @@ const { resolve } = require('path');
module.exports = function (grunt) {
grunt.registerTask('test:jest', function () {
const done = this.async();
- runJest(resolve(__dirname, '../scripts/jest.js'), ['--maxWorkers=10']).then(done, done);
+ runJest(resolve(__dirname, '../scripts/jest.js')).then(done, done);
});
grunt.registerTask('test:jest_integration', function () {
@@ -30,10 +30,10 @@ module.exports = function (grunt) {
runJest(resolve(__dirname, '../scripts/jest_integration.js')).then(done, done);
});
- function runJest(jestScript, args = []) {
+ function runJest(jestScript) {
const serverCmd = {
cmd: 'node',
- args: [jestScript, '--ci', ...args],
+ args: [jestScript, '--ci'],
opts: { stdio: 'inherit' },
};
diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js
index 949a01ff7873a..47741c1ab8a0d 100644
--- a/test/functional/apps/discover/_discover.js
+++ b/test/functional/apps/discover/_discover.js
@@ -96,25 +96,32 @@ export default function ({ getService, getPageObjects }) {
it('should modify the time range when a bar is clicked', async function () {
await PageObjects.timePicker.setDefaultAbsoluteRange();
await PageObjects.discover.clickHistogramBar();
+ await PageObjects.discover.waitUntilSearchingHasFinished();
const time = await PageObjects.timePicker.getTimeConfig();
expect(time.start).to.be('Sep 21, 2015 @ 09:00:00.000');
expect(time.end).to.be('Sep 21, 2015 @ 12:00:00.000');
- const rowData = await PageObjects.discover.getDocTableField(1);
- expect(rowData).to.have.string('Sep 21, 2015 @ 11:59:22.316');
+ await retry.waitFor('doc table to contain the right search result', async () => {
+ const rowData = await PageObjects.discover.getDocTableField(1);
+ log.debug(`The first timestamp value in doc table: ${rowData}`);
+ return rowData.includes('Sep 21, 2015 @ 11:59:22.316');
+ });
});
it('should modify the time range when the histogram is brushed', async function () {
await PageObjects.timePicker.setDefaultAbsoluteRange();
await PageObjects.discover.brushHistogram();
+ await PageObjects.discover.waitUntilSearchingHasFinished();
const newDurationHours = await PageObjects.timePicker.getTimeDurationInHours();
expect(Math.round(newDurationHours)).to.be(24);
- const rowData = await PageObjects.discover.getDocTableField(1);
- log.debug(`The first timestamp value in doc table: ${rowData}`);
- expect(Date.parse(rowData)).to.be.within(
- Date.parse('Sep 20, 2015 @ 17:30:00.000'),
- Date.parse('Sep 20, 2015 @ 23:30:00.000')
- );
+
+ await retry.waitFor('doc table to contain the right search result', async () => {
+ const rowData = await PageObjects.discover.getDocTableField(1);
+ log.debug(`The first timestamp value in doc table: ${rowData}`);
+ const dateParsed = Date.parse(rowData);
+ //compare against the parsed date of Sep 20, 2015 @ 17:30:00.000 and Sep 20, 2015 @ 23:30:00.000
+ return dateParsed >= 1442770200000 && dateParsed <= 1442791800000;
+ });
});
it('should show correct initial chart interval of Auto', async function () {
diff --git a/test/functional/apps/discover/_doc_navigation.js b/test/functional/apps/discover/_doc_navigation.js
index f6a092ecb79a8..9bcf7fd2d73b5 100644
--- a/test/functional/apps/discover/_doc_navigation.js
+++ b/test/functional/apps/discover/_doc_navigation.js
@@ -20,14 +20,20 @@
import expect from '@kbn/expect';
export default function ({ getService, getPageObjects }) {
+ const log = getService('log');
const docTable = getService('docTable');
+ const filterBar = getService('filterBar');
const testSubjects = getService('testSubjects');
- const PageObjects = getPageObjects(['common', 'discover', 'timePicker']);
+ const PageObjects = getPageObjects(['common', 'discover', 'timePicker', 'context']);
const esArchiver = getService('esArchiver');
const retry = getService('retry');
- describe('doc link in discover', function contextSize() {
- before(async function () {
+ // Flaky: https://github.com/elastic/kibana/issues/71216
+ describe.skip('doc link in discover', function contextSize() {
+ beforeEach(async function () {
+ log.debug('load kibana index with default index pattern');
+ await esArchiver.loadIfNeeded('discover');
+
await esArchiver.loadIfNeeded('logstash_functional');
await PageObjects.common.navigateToApp('discover');
await PageObjects.timePicker.setDefaultAbsoluteRange();
@@ -50,5 +56,27 @@ export default function ({ getService, getPageObjects }) {
const hasDocHit = await testSubjects.exists('doc-hit');
expect(hasDocHit).to.be(true);
});
+
+ it('add filter should create an exists filter if value is null (#7189)', async function () {
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+ // Filter special document
+ await filterBar.addFilter('agent', 'is', 'Missing/Fields');
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+
+ // navigate to the doc view
+ await docTable.clickRowToggle({ rowIndex: 0 });
+
+ const details = await docTable.getDetailsRow();
+ await docTable.addInclusiveFilter(details, 'referer');
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+
+ const hasInclusiveFilter = await filterBar.hasFilter('referer', 'exists', true, false, true);
+ expect(hasInclusiveFilter).to.be(true);
+
+ await docTable.removeInclusiveFilter(details, 'referer');
+ await PageObjects.discover.waitUntilSearchingHasFinished();
+ const hasExcludeFilter = await filterBar.hasFilter('referer', 'exists', true, false, false);
+ expect(hasExcludeFilter).to.be(true);
+ });
});
}
diff --git a/test/functional/apps/discover/_saved_queries.js b/test/functional/apps/discover/_saved_queries.js
index 61bb5f7cfee6f..6b423bf6eeb1d 100644
--- a/test/functional/apps/discover/_saved_queries.js
+++ b/test/functional/apps/discover/_saved_queries.js
@@ -20,6 +20,7 @@
import expect from '@kbn/expect';
export default function ({ getService, getPageObjects }) {
+ const retry = getService('retry');
const log = getService('log');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
@@ -93,7 +94,10 @@ export default function ({ getService, getPageObjects }) {
expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(true);
expect(timePickerValues.start).to.not.eql(PageObjects.timePicker.defaultStartTime);
expect(timePickerValues.end).to.not.eql(PageObjects.timePicker.defaultEndTime);
- expect(await PageObjects.discover.getHitCount()).to.be('2,792');
+ await retry.waitFor(
+ 'the right hit count',
+ async () => (await PageObjects.discover.getHitCount()) === '2,792'
+ );
expect(await savedQueryManagementComponent.getCurrentlyLoadedQueryID()).to.be('OkResponse');
});
@@ -149,7 +153,6 @@ export default function ({ getService, getPageObjects }) {
expect(await queryBar.getQueryString()).to.eql('');
});
- // https://github.com/elastic/kibana/issues/63505
it('allows clearing if non default language was remembered in localstorage', async () => {
await queryBar.switchQueryLanguage('lucene');
await PageObjects.common.navigateToApp('discover'); // makes sure discovered is reloaded without any state in url
@@ -160,9 +163,7 @@ export default function ({ getService, getPageObjects }) {
await queryBar.expectQueryLanguageOrFail('lucene');
});
- // fails: bug in discover https://github.com/elastic/kibana/issues/63561
- // unskip this test when bug is fixed
- it.skip('changing language removes saved query', async () => {
+ it('changing language removes saved query', async () => {
await savedQueryManagementComponent.loadSavedQuery('OkResponse');
await queryBar.switchQueryLanguage('lucene');
expect(await queryBar.getQueryString()).to.eql('');
diff --git a/test/functional/apps/home/_navigation.ts b/test/functional/apps/home/_navigation.ts
index cfe4f9cc3e014..b8fa5b184cd1f 100644
--- a/test/functional/apps/home/_navigation.ts
+++ b/test/functional/apps/home/_navigation.ts
@@ -26,21 +26,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const PageObjects = getPageObjects(['common', 'header', 'home', 'timePicker']);
const appsMenu = getService('appsMenu');
const esArchiver = getService('esArchiver');
- const kibanaServer = getService('kibanaServer');
describe('Kibana browser back navigation should work', function describeIndexTests() {
before(async () => {
await esArchiver.loadIfNeeded('discover');
await esArchiver.loadIfNeeded('logstash_functional');
- if (browser.isInternetExplorer) {
- await kibanaServer.uiSettings.replace({ 'state:storeInSessionStorage': false });
- }
- });
-
- after(async () => {
- if (browser.isInternetExplorer) {
- await kibanaServer.uiSettings.replace({ 'state:storeInSessionStorage': true });
- }
});
it('detect navigate back issues', async () => {
diff --git a/test/functional/apps/visualize/_data_table_nontimeindex.js b/test/functional/apps/visualize/_data_table_nontimeindex.js
index d64629a65c2c3..fd06257a91ff4 100644
--- a/test/functional/apps/visualize/_data_table_nontimeindex.js
+++ b/test/functional/apps/visualize/_data_table_nontimeindex.js
@@ -112,8 +112,7 @@ export default function ({ getService, getPageObjects }) {
expect(data.trim().split('\n')).to.be.eql(['14,004 1,412.6']);
});
- // bug https://github.com/elastic/kibana/issues/68977
- describe.skip('data table with date histogram', async () => {
+ describe('data table with date histogram', async () => {
before(async () => {
await PageObjects.visualize.navigateToNewVisualization();
await PageObjects.visualize.clickDataTable();
@@ -123,7 +122,7 @@ export default function ({ getService, getPageObjects }) {
await PageObjects.visEditor.clickBucket('Split rows');
await PageObjects.visEditor.selectAggregation('Date Histogram');
await PageObjects.visEditor.selectField('@timestamp');
- await PageObjects.visEditor.setInterval('Daily');
+ await PageObjects.visEditor.setInterval('Day');
await PageObjects.visEditor.clickGo();
});
diff --git a/test/functional/fixtures/es_archiver/logstash_functional/data.json.gz b/test/functional/fixtures/es_archiver/logstash_functional/data.json.gz
index a212c34e2ead6..a4f889da61128 100644
Binary files a/test/functional/fixtures/es_archiver/logstash_functional/data.json.gz and b/test/functional/fixtures/es_archiver/logstash_functional/data.json.gz differ
diff --git a/test/functional/page_objects/management/saved_objects_page.ts b/test/functional/page_objects/management/saved_objects_page.ts
index d058695ea6819..03d21aa4aa52f 100644
--- a/test/functional/page_objects/management/saved_objects_page.ts
+++ b/test/functional/page_objects/management/saved_objects_page.ts
@@ -87,13 +87,15 @@ export function SavedObjectsPageProvider({ getService, getPageObjects }: FtrProv
async waitTableIsLoaded() {
return retry.try(async () => {
- const exists = await find.existsByDisplayedByCssSelector(
- '*[data-test-subj="savedObjectsTable"] .euiBasicTable-loading'
+ const isLoaded = await find.existsByDisplayedByCssSelector(
+ '*[data-test-subj="savedObjectsTable"] :not(.euiBasicTable-loading)'
);
- if (exists) {
+
+ if (isLoaded) {
+ return true;
+ } else {
throw new Error('Waiting');
}
- return true;
});
}
diff --git a/test/functional/page_objects/time_picker.ts b/test/functional/page_objects/time_picker.ts
index 7ef291c8c7005..8a726cee444c1 100644
--- a/test/functional/page_objects/time_picker.ts
+++ b/test/functional/page_objects/time_picker.ts
@@ -98,13 +98,6 @@ export function TimePickerProvider({ getService, getPageObjects }: FtrProviderCo
const input = await testSubjects.find(dataTestSubj);
await input.clearValue();
await input.type(value);
- } else if (browser.isInternetExplorer) {
- const input = await testSubjects.find(dataTestSubj);
- const currentValue = await input.getAttribute('value');
- await input.type(browser.keys.ARROW_RIGHT.repeat(currentValue.length));
- await input.type(browser.keys.BACK_SPACE.repeat(currentValue.length));
- await input.type(value);
- await input.click();
} else {
await testSubjects.setValue(dataTestSubj, value);
}
diff --git a/test/functional/services/common/browser.ts b/test/functional/services/common/browser.ts
index 2d35551b04808..c38ac771e4162 100644
--- a/test/functional/services/common/browser.ts
+++ b/test/functional/services/common/browser.ts
@@ -34,8 +34,6 @@ export async function BrowserProvider({ getService }: FtrProviderContext) {
const log = getService('log');
const { driver, browserType } = await getService('__webdriver__').init();
- const isW3CEnabled = (driver as any).executor_.w3c === true;
-
return new (class BrowserService {
/**
* Keyboard events
@@ -53,19 +51,12 @@ export async function BrowserProvider({ getService }: FtrProviderContext) {
public readonly isFirefox: boolean = browserType === Browsers.Firefox;
- public readonly isInternetExplorer: boolean = browserType === Browsers.InternetExplorer;
-
- /**
- * Is WebDriver instance W3C compatible
- */
- isW3CEnabled = isW3CEnabled;
-
/**
* Returns instance of Actions API based on driver w3c flag
* https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_WebDriver.html#actions
*/
public getActions() {
- return this.isW3CEnabled ? driver.actions() : driver.actions({ bridge: true });
+ return driver.actions();
}
/**
@@ -164,12 +155,7 @@ export async function BrowserProvider({ getService }: FtrProviderContext) {
*/
public async getCurrentUrl() {
// strip _t=Date query param when url is read
- let current: string;
- if (this.isInternetExplorer) {
- current = await driver.executeScript('return window.document.location.href');
- } else {
- current = await driver.getCurrentUrl();
- }
+ const current = await driver.getCurrentUrl();
const currentWithoutTime = modifyUrl(current, (parsed) => {
delete (parsed.query as any)._t;
return void 0;
@@ -214,15 +200,8 @@ export async function BrowserProvider({ getService }: FtrProviderContext) {
* @return {Promise}
*/
public async moveMouseTo(point: { x: number; y: number }): Promise {
- if (this.isW3CEnabled) {
- await this.getActions().move({ x: 0, y: 0 }).perform();
- await this.getActions().move({ x: point.x, y: point.y, origin: Origin.POINTER }).perform();
- } else {
- await this.getActions()
- .pause(this.getActions().mouse)
- .move({ x: point.x, y: point.y, origin: Origin.POINTER })
- .perform();
- }
+ await this.getActions().move({ x: 0, y: 0 }).perform();
+ await this.getActions().move({ x: point.x, y: point.y, origin: Origin.POINTER }).perform();
}
/**
@@ -237,44 +216,20 @@ export async function BrowserProvider({ getService }: FtrProviderContext) {
from: { offset?: { x: any; y: any }; location: any },
to: { offset?: { x: any; y: any }; location: any }
) {
- if (this.isW3CEnabled) {
- // The offset should be specified in pixels relative to the center of the element's bounding box
- const getW3CPoint = (data: any) => {
- if (!data.offset) {
- data.offset = {};
- }
- return data.location instanceof WebElementWrapper
- ? { x: data.offset.x || 0, y: data.offset.y || 0, origin: data.location._webElement }
- : { x: data.location.x, y: data.location.y, origin: Origin.POINTER };
- };
-
- const startPoint = getW3CPoint(from);
- const endPoint = getW3CPoint(to);
- await this.getActions().move({ x: 0, y: 0 }).perform();
- return await this.getActions().move(startPoint).press().move(endPoint).release().perform();
- } else {
- // The offset should be specified in pixels relative to the top-left corner of the element's bounding box
- const getOffset: any = (offset: { x: number; y: number }) =>
- offset ? { x: offset.x || 0, y: offset.y || 0 } : { x: 0, y: 0 };
-
- if (from.location instanceof WebElementWrapper === false) {
- throw new Error('Dragging point should be WebElementWrapper instance');
- } else if (typeof to.location.x === 'number') {
- return await this.getActions()
- .move({ origin: from.location._webElement })
- .press()
- .move({ x: to.location.x, y: to.location.y, origin: Origin.POINTER })
- .release()
- .perform();
- } else {
- return await new LegacyActionSequence(driver)
- .mouseMove(from.location._webElement, getOffset(from.offset))
- .mouseDown()
- .mouseMove(to.location._webElement, getOffset(to.offset))
- .mouseUp()
- .perform();
+ // The offset should be specified in pixels relative to the center of the element's bounding box
+ const getW3CPoint = (data: any) => {
+ if (!data.offset) {
+ data.offset = {};
}
- }
+ return data.location instanceof WebElementWrapper
+ ? { x: data.offset.x || 0, y: data.offset.y || 0, origin: data.location._webElement }
+ : { x: data.location.x, y: data.location.y, origin: Origin.POINTER };
+ };
+
+ const startPoint = getW3CPoint(from);
+ const endPoint = getW3CPoint(to);
+ await this.getActions().move({ x: 0, y: 0 }).perform();
+ return await this.getActions().move(startPoint).press().move(endPoint).release().perform();
}
/**
@@ -341,19 +296,11 @@ export async function BrowserProvider({ getService }: FtrProviderContext) {
* @return {Promise}
*/
public async clickMouseButton(point: { x: number; y: number }) {
- if (this.isW3CEnabled) {
- await this.getActions().move({ x: 0, y: 0 }).perform();
- await this.getActions()
- .move({ x: point.x, y: point.y, origin: Origin.POINTER })
- .click()
- .perform();
- } else {
- await this.getActions()
- .pause(this.getActions().mouse)
- .move({ x: point.x, y: point.y, origin: Origin.POINTER })
- .click()
- .perform();
- }
+ await this.getActions().move({ x: 0, y: 0 }).perform();
+ await this.getActions()
+ .move({ x: point.x, y: point.y, origin: Origin.POINTER })
+ .click()
+ .perform();
}
/**
diff --git a/test/functional/services/doc_table.ts b/test/functional/services/doc_table.ts
index 52593de68705b..1ac8de69ee5f4 100644
--- a/test/functional/services/doc_table.ts
+++ b/test/functional/services/doc_table.ts
@@ -58,6 +58,11 @@ export function DocTableProvider({ getService, getPageObjects }: FtrProviderCont
: (await this.getBodyRows())[options.rowIndex];
}
+ public async getDetailsRow(): Promise {
+ const table = await this.getTable();
+ return await table.findByCssSelector('[data-test-subj~="docTableDetailsRow"]');
+ }
+
public async getAnchorDetailsRow(): Promise {
const table = await this.getTable();
return await table.findByCssSelector(
@@ -133,6 +138,22 @@ export function DocTableProvider({ getService, getPageObjects }: FtrProviderCont
await PageObjects.header.awaitGlobalLoadingIndicatorHidden();
}
+ public async getRemoveInclusiveFilterButton(
+ tableDocViewRow: WebElementWrapper
+ ): Promise {
+ return await tableDocViewRow.findByTestSubject(`~removeInclusiveFilterButton`);
+ }
+
+ public async removeInclusiveFilter(
+ detailsRow: WebElementWrapper,
+ fieldName: WebElementWrapper
+ ): Promise {
+ const tableDocViewRow = await this.getTableDocViewRow(detailsRow, fieldName);
+ const addInclusiveFilterButton = await this.getRemoveInclusiveFilterButton(tableDocViewRow);
+ await addInclusiveFilterButton.click();
+ await PageObjects.header.awaitGlobalLoadingIndicatorHidden();
+ }
+
public async getAddExistsFilterButton(
tableDocViewRow: WebElementWrapper
): Promise {
diff --git a/test/functional/services/filter_bar.ts b/test/functional/services/filter_bar.ts
index f6531f8d872c2..98ab1babd60fe 100644
--- a/test/functional/services/filter_bar.ts
+++ b/test/functional/services/filter_bar.ts
@@ -31,17 +31,21 @@ export function FilterBarProvider({ getService, getPageObjects }: FtrProviderCon
* @param key field name
* @param value filter value
* @param enabled filter status
+ * @param pinned filter pinned status
+ * @param negated filter including or excluding value
*/
public async hasFilter(
key: string,
value: string,
enabled: boolean = true,
- pinned: boolean = false
+ pinned: boolean = false,
+ negated: boolean = false
): Promise {
const filterActivationState = enabled ? 'enabled' : 'disabled';
const filterPinnedState = pinned ? 'pinned' : 'unpinned';
+ const filterNegatedState = negated ? 'filter-negated' : '';
return testSubjects.exists(
- `filter filter-${filterActivationState} filter-key-${key} filter-value-${value} filter-${filterPinnedState}`,
+ `filter filter-${filterActivationState} filter-key-${key} filter-value-${value} filter-${filterPinnedState} ${filterNegatedState}`,
{
allowHidden: true,
}
diff --git a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts
index 281a412653bd0..5011235551bd8 100644
--- a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts
+++ b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts
@@ -47,7 +47,6 @@ const RETRY_CLICK_RETRY_ON_ERRORS = [
export class WebElementWrapper {
private By = By;
private Keys = Key;
- public isW3CEnabled: boolean = (this.driver as any).executor_.w3c === true;
public isChromium: boolean = [Browsers.Chrome, Browsers.ChromiumEdge].includes(this.browserType);
public static create(
@@ -141,7 +140,7 @@ export class WebElementWrapper {
}
private getActions() {
- return this.isW3CEnabled ? this.driver.actions() : this.driver.actions({ bridge: true });
+ return this.driver.actions();
}
/**
@@ -233,9 +232,6 @@ export class WebElementWrapper {
* @default { withJS: false }
*/
async clearValue(options: ClearOptions = { withJS: false }) {
- if (this.browserType === Browsers.InternetExplorer) {
- return this.clearValueWithKeyboard();
- }
await this.retryCall(async function clearValue(wrapper) {
if (wrapper.isChromium || options.withJS) {
// https://bugs.chromium.org/p/chromedriver/issues/detail?id=2702
@@ -252,16 +248,6 @@ export class WebElementWrapper {
* @default { charByChar: false }
*/
async clearValueWithKeyboard(options: TypeOptions = { charByChar: false }) {
- if (this.browserType === Browsers.InternetExplorer) {
- const value = await this.getAttribute('value');
- // For IE testing, the text field gets clicked in the middle so
- // first go HOME and then DELETE all chars
- await this.pressKeys(this.Keys.HOME);
- for (let i = 0; i <= value.length; i++) {
- await this.pressKeys(this.Keys.DELETE);
- }
- return;
- }
if (options.charByChar === true) {
const value = await this.getAttribute('value');
for (let i = 0; i <= value.length; i++) {
@@ -429,19 +415,11 @@ export class WebElementWrapper {
public async moveMouseTo(options = { xOffset: 0, yOffset: 0 }) {
await this.retryCall(async function moveMouseTo(wrapper) {
await wrapper.scrollIntoViewIfNecessary();
- if (wrapper.isW3CEnabled) {
- await wrapper.getActions().move({ x: 0, y: 0 }).perform();
- await wrapper
- .getActions()
- .move({ x: options.xOffset, y: options.yOffset, origin: wrapper._webElement })
- .perform();
- } else {
- await wrapper
- .getActions()
- .pause(wrapper.getActions().mouse)
- .move({ x: options.xOffset, y: options.yOffset, origin: wrapper._webElement })
- .perform();
- }
+ await wrapper.getActions().move({ x: 0, y: 0 }).perform();
+ await wrapper
+ .getActions()
+ .move({ x: options.xOffset, y: options.yOffset, origin: wrapper._webElement })
+ .perform();
});
}
@@ -456,21 +434,12 @@ export class WebElementWrapper {
public async clickMouseButton(options = { xOffset: 0, yOffset: 0 }) {
await this.retryCall(async function clickMouseButton(wrapper) {
await wrapper.scrollIntoViewIfNecessary();
- if (wrapper.isW3CEnabled) {
- await wrapper.getActions().move({ x: 0, y: 0 }).perform();
- await wrapper
- .getActions()
- .move({ x: options.xOffset, y: options.yOffset, origin: wrapper._webElement })
- .click()
- .perform();
- } else {
- await wrapper
- .getActions()
- .pause(wrapper.getActions().mouse)
- .move({ x: options.xOffset, y: options.yOffset, origin: wrapper._webElement })
- .click()
- .perform();
- }
+ await wrapper.getActions().move({ x: 0, y: 0 }).perform();
+ await wrapper
+ .getActions()
+ .move({ x: options.xOffset, y: options.yOffset, origin: wrapper._webElement })
+ .click()
+ .perform();
});
}
diff --git a/test/functional/services/remote/browsers.ts b/test/functional/services/remote/browsers.ts
index aa6e364d0a09d..f7942e708a3bb 100644
--- a/test/functional/services/remote/browsers.ts
+++ b/test/functional/services/remote/browsers.ts
@@ -20,6 +20,5 @@
export enum Browsers {
Chrome = 'chrome',
Firefox = 'firefox',
- InternetExplorer = 'ie',
ChromiumEdge = 'msedge',
}
diff --git a/test/functional/services/remote/remote.ts b/test/functional/services/remote/remote.ts
index 99643929c4682..a45403e31095c 100644
--- a/test/functional/services/remote/remote.ts
+++ b/test/functional/services/remote/remote.ts
@@ -64,15 +64,12 @@ export async function RemoteProvider({ getService }: FtrProviderContext) {
};
const { driver, consoleLog$ } = await initWebDriver(log, browserType, lifecycle, browserConfig);
- const isW3CEnabled = (driver as any).executor_.w3c;
-
const caps = await driver.getCapabilities();
- const browserVersion = caps.get(isW3CEnabled ? 'browserVersion' : 'version');
log.info(
- `Remote initialized: ${caps.get(
- 'browserName'
- )} ${browserVersion}, w3c compliance=${isW3CEnabled}, collectingCoverage=${collectCoverage}`
+ `Remote initialized: ${caps.get('browserName')} ${caps.get(
+ 'browserVersion'
+ )}, collectingCoverage=${collectCoverage}`
);
if ([Browsers.Chrome, Browsers.ChromiumEdge].includes(browserType)) {
diff --git a/test/functional/services/remote/webdriver.ts b/test/functional/services/remote/webdriver.ts
index 27814060e70c1..0611c80f59b92 100644
--- a/test/functional/services/remote/webdriver.ts
+++ b/test/functional/services/remote/webdriver.ts
@@ -17,7 +17,7 @@
* under the License.
*/
-import { delimiter, resolve } from 'path';
+import { resolve } from 'path';
import Fs from 'fs';
import * as Rx from 'rxjs';
@@ -28,7 +28,7 @@ import { delay } from 'bluebird';
import chromeDriver from 'chromedriver';
// @ts-ignore types not available
import geckoDriver from 'geckodriver';
-import { Builder, Capabilities, logging } from 'selenium-webdriver';
+import { Builder, logging } from 'selenium-webdriver';
import chrome from 'selenium-webdriver/chrome';
import firefox from 'selenium-webdriver/firefox';
import edge from 'selenium-webdriver/edge';
@@ -47,6 +47,7 @@ import { Browsers } from './browsers';
const throttleOption: string = process.env.TEST_THROTTLE_NETWORK as string;
const headlessBrowser: string = process.env.TEST_BROWSER_HEADLESS as string;
+const browserBinaryPath: string = process.env.TEST_BROWSER_BINARY_PATH as string;
const remoteDebug: string = process.env.TEST_REMOTE_DEBUG as string;
const certValidation: string = process.env.NODE_TLS_REJECT_UNAUTHORIZED as string;
const SECOND = 1000;
@@ -54,10 +55,8 @@ const MINUTE = 60 * SECOND;
const NO_QUEUE_COMMANDS = ['getLog', 'getStatus', 'newSession', 'quit'];
const downloadDir = resolve(REPO_ROOT, 'target/functional-tests/downloads');
const chromiumDownloadPrefs = {
- prefs: {
- 'download.default_directory': downloadDir,
- 'download.prompt_for_download': false,
- },
+ 'download.default_directory': downloadDir,
+ 'download.prompt_for_download': false,
};
/**
@@ -88,12 +87,13 @@ async function attemptToCreateCommand(
) {
const attemptId = ++attemptCounter;
log.debug('[webdriver] Creating session');
+ const remoteSessionUrl = process.env.REMOTE_SESSION_URL;
const buildDriverInstance = async () => {
switch (browserType) {
case 'chrome': {
- const chromeCapabilities = Capabilities.chrome();
- const chromeOptions = [
+ const chromeOptions = new chrome.Options();
+ chromeOptions.addArguments(
// Disables the sandbox for all process types that are normally sandboxed.
'no-sandbox',
// Launches URL in new browser window.
@@ -103,41 +103,58 @@ async function attemptToCreateCommand(
// Use fake device for Media Stream to replace actual camera and microphone.
'use-fake-device-for-media-stream',
// Bypass the media stream infobar by selecting the default device for media streams (e.g. WebRTC). Works with --use-fake-device-for-media-stream.
- 'use-fake-ui-for-media-stream',
- ];
+ 'use-fake-ui-for-media-stream'
+ );
+
if (process.platform === 'linux') {
// The /dev/shm partition is too small in certain VM environments, causing
// Chrome to fail or crash. Use this flag to work-around this issue
// (a temporary directory will always be used to create anonymous shared memory files).
- chromeOptions.push('disable-dev-shm-usage');
+ chromeOptions.addArguments('disable-dev-shm-usage');
}
+
if (headlessBrowser === '1') {
// Use --disable-gpu to avoid an error from a missing Mesa library, as per
// See: https://chromium.googlesource.com/chromium/src/+/lkgr/headless/README.md
- chromeOptions.push('headless', 'disable-gpu');
+ chromeOptions.headless();
+ chromeOptions.addArguments('disable-gpu');
}
+
if (certValidation === '0') {
- chromeOptions.push('ignore-certificate-errors');
+ chromeOptions.addArguments('ignore-certificate-errors');
}
if (remoteDebug === '1') {
// Visit chrome://inspect in chrome to remotely view/debug
- chromeOptions.push('headless', 'disable-gpu', 'remote-debugging-port=9222');
+ chromeOptions.headless();
+ chromeOptions.addArguments('disable-gpu', 'remote-debugging-port=9222');
}
- chromeCapabilities.set('goog:chromeOptions', {
- w3c: true,
- args: chromeOptions,
- ...chromiumDownloadPrefs,
- });
- chromeCapabilities.set('unexpectedAlertBehaviour', 'accept');
- chromeCapabilities.set('goog:loggingPrefs', { browser: 'ALL' });
- chromeCapabilities.setAcceptInsecureCerts(config.acceptInsecureCerts);
- const session = await new Builder()
- .forBrowser(browserType)
- .withCapabilities(chromeCapabilities)
- .setChromeService(new chrome.ServiceBuilder(chromeDriver.path).enableVerboseLogging())
- .build();
+ if (browserBinaryPath) {
+ chromeOptions.setChromeBinaryPath(browserBinaryPath);
+ }
+
+ const prefs = new logging.Preferences();
+ prefs.setLevel(logging.Type.BROWSER, logging.Level.ALL);
+ chromeOptions.setUserPreferences(chromiumDownloadPrefs);
+ chromeOptions.setLoggingPrefs(prefs);
+ chromeOptions.set('unexpectedAlertBehaviour', 'accept');
+ chromeOptions.setAcceptInsecureCerts(config.acceptInsecureCerts);
+
+ let session;
+ if (remoteSessionUrl) {
+ session = await new Builder()
+ .forBrowser(browserType)
+ .setChromeOptions(chromeOptions)
+ .usingServer(remoteSessionUrl)
+ .build();
+ } else {
+ session = await new Builder()
+ .forBrowser(browserType)
+ .setChromeOptions(chromeOptions)
+ .setChromeService(new chrome.ServiceBuilder(chromeDriver.path).enableVerboseLogging())
+ .build();
+ }
return {
session,
@@ -169,7 +186,7 @@ async function attemptToCreateCommand(
edgeOptions.setBinaryPath(edgePaths.browserPath);
const options = edgeOptions.get('ms:edgeOptions');
// overriding options to include preferences
- Object.assign(options, chromiumDownloadPrefs);
+ Object.assign(options, { prefs: chromiumDownloadPrefs });
edgeOptions.set('ms:edgeOptions', options);
const session = await new Builder()
.forBrowser('MicrosoftEdge')
@@ -269,32 +286,6 @@ async function attemptToCreateCommand(
};
}
- case 'ie': {
- // https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/ie_exports_Options.html
- const driverPath = require.resolve('iedriver/lib/iedriver');
- process.env.PATH = driverPath + delimiter + process.env.PATH;
-
- const ieCapabilities = Capabilities.ie();
- ieCapabilities.set('se:ieOptions', {
- 'ie.ensureCleanSession': true,
- ignoreProtectedModeSettings: true,
- ignoreZoomSetting: false, // requires us to have 100% zoom level
- nativeEvents: true, // need this for values to stick but it requires 100% scaling and window focus
- requireWindowFocus: true,
- logLevel: 'TRACE',
- });
-
- const session = await new Builder()
- .forBrowser(browserType)
- .withCapabilities(ieCapabilities)
- .build();
-
- return {
- session,
- consoleLog$: Rx.EMPTY,
- };
- }
-
default:
throw new Error(`${browserType} is not supported yet`);
}
diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/kibana.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/kibana.json
index f0c1c3a34fbc0..7eafb185617c4 100644
--- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/kibana.json
+++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/kibana.json
@@ -9,5 +9,8 @@
"expressions"
],
"server": false,
- "ui": true
+ "ui": true,
+ "requiredBundles": [
+ "inspector"
+ ]
}
diff --git a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json
index 535aecba26770..f3e5520a14fe2 100644
--- a/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json
+++ b/test/interpreter_functional/plugins/kbn_tp_run_pipeline/package.json
@@ -8,7 +8,7 @@
},
"license": "Apache-2.0",
"dependencies": {
- "@elastic/eui": "24.1.0",
+ "@elastic/eui": "26.3.1",
"react": "^16.12.0",
"react-dom": "^16.12.0"
},
diff --git a/test/plugin_functional/plugins/app_link_test/kibana.json b/test/plugin_functional/plugins/app_link_test/kibana.json
index 8cdc464abfec1..5384d4fee1508 100644
--- a/test/plugin_functional/plugins/app_link_test/kibana.json
+++ b/test/plugin_functional/plugins/app_link_test/kibana.json
@@ -3,5 +3,6 @@
"version": "0.0.1",
"kibanaVersion": "kibana",
"server": false,
- "ui": true
+ "ui": true,
+ "requiredBundles": ["kibanaReact"]
}
diff --git a/test/plugin_functional/plugins/kbn_sample_panel_action/kibana.json b/test/plugin_functional/plugins/kbn_sample_panel_action/kibana.json
index 109afbcd5dabd..08ce182aa0293 100644
--- a/test/plugin_functional/plugins/kbn_sample_panel_action/kibana.json
+++ b/test/plugin_functional/plugins/kbn_sample_panel_action/kibana.json
@@ -5,5 +5,6 @@
"configPath": ["kbn_sample_panel_action"],
"server": false,
"ui": true,
- "requiredPlugins": ["uiActions", "embeddable"]
-}
\ No newline at end of file
+ "requiredPlugins": ["uiActions", "embeddable"],
+ "requiredBundles": ["kibanaReact"]
+}
diff --git a/test/plugin_functional/plugins/kbn_sample_panel_action/package.json b/test/plugin_functional/plugins/kbn_sample_panel_action/package.json
index 612ae3806177c..b9c5b3bc5b836 100644
--- a/test/plugin_functional/plugins/kbn_sample_panel_action/package.json
+++ b/test/plugin_functional/plugins/kbn_sample_panel_action/package.json
@@ -8,7 +8,7 @@
},
"license": "Apache-2.0",
"dependencies": {
- "@elastic/eui": "24.1.0",
+ "@elastic/eui": "26.3.1",
"react": "^16.12.0"
},
"scripts": {
diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json
index 0a6b5fb185d30..95fafdf221c64 100644
--- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json
+++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/package.json
@@ -8,7 +8,7 @@
},
"license": "Apache-2.0",
"dependencies": {
- "@elastic/eui": "24.1.0",
+ "@elastic/eui": "26.3.1",
"react": "^16.12.0"
},
"scripts": {
diff --git a/test/scripts/checks/doc_api_changes.sh b/test/scripts/checks/doc_api_changes.sh
deleted file mode 100755
index 503d12b2f6d73..0000000000000
--- a/test/scripts/checks/doc_api_changes.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:checkDocApiChanges
diff --git a/test/scripts/checks/file_casing.sh b/test/scripts/checks/file_casing.sh
deleted file mode 100755
index 513664263791b..0000000000000
--- a/test/scripts/checks/file_casing.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:checkFileCasing
diff --git a/test/scripts/checks/i18n.sh b/test/scripts/checks/i18n.sh
deleted file mode 100755
index 7a6fd46c46c76..0000000000000
--- a/test/scripts/checks/i18n.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:i18nCheck
diff --git a/test/scripts/checks/licenses.sh b/test/scripts/checks/licenses.sh
deleted file mode 100755
index a08d7d07a24a1..0000000000000
--- a/test/scripts/checks/licenses.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:licenses
diff --git a/test/scripts/checks/lock_file_symlinks.sh b/test/scripts/checks/lock_file_symlinks.sh
deleted file mode 100755
index 1d43d32c9feb8..0000000000000
--- a/test/scripts/checks/lock_file_symlinks.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:checkLockfileSymlinks
diff --git a/test/scripts/checks/test_hardening.sh b/test/scripts/checks/test_hardening.sh
deleted file mode 100755
index 9184758577654..0000000000000
--- a/test/scripts/checks/test_hardening.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:test_hardening
diff --git a/test/scripts/checks/test_projects.sh b/test/scripts/checks/test_projects.sh
deleted file mode 100755
index 5f9aafe80e10e..0000000000000
--- a/test/scripts/checks/test_projects.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:test_projects
diff --git a/test/scripts/checks/ts_projects.sh b/test/scripts/checks/ts_projects.sh
deleted file mode 100755
index d667c753baec2..0000000000000
--- a/test/scripts/checks/ts_projects.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:checkTsProjects
diff --git a/test/scripts/checks/type_check.sh b/test/scripts/checks/type_check.sh
deleted file mode 100755
index 07c49638134be..0000000000000
--- a/test/scripts/checks/type_check.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:typeCheck
diff --git a/test/scripts/checks/verify_dependency_versions.sh b/test/scripts/checks/verify_dependency_versions.sh
deleted file mode 100755
index b73a71e7ff7fd..0000000000000
--- a/test/scripts/checks/verify_dependency_versions.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:verifyDependencyVersions
diff --git a/test/scripts/checks/verify_notice.sh b/test/scripts/checks/verify_notice.sh
deleted file mode 100755
index 9f8343e540861..0000000000000
--- a/test/scripts/checks/verify_notice.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:verifyNotice
diff --git a/test/scripts/jenkins_build_kbn_sample_panel_action.sh b/test/scripts/jenkins_build_kbn_sample_panel_action.sh
old mode 100755
new mode 100644
diff --git a/test/scripts/jenkins_build_kibana.sh b/test/scripts/jenkins_build_kibana.sh
index f449986713f97..2310a35f94f33 100755
--- a/test/scripts/jenkins_build_kibana.sh
+++ b/test/scripts/jenkins_build_kibana.sh
@@ -2,9 +2,13 @@
source src/dev/ci_setup/setup_env.sh
-if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then
- ./test/scripts/jenkins_build_plugins.sh
-fi
+echo " -> building kibana platform plugins"
+node scripts/build_kibana_platform_plugins \
+ --oss \
+ --filter '!alertingExample' \
+ --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \
+ --scan-dir "$KIBANA_DIR/test/interpreter_functional/plugins" \
+ --verbose;
# doesn't persist, also set in kibanaPipeline.groovy
export KBN_NP_PLUGINS_BUILT=true
@@ -16,7 +20,4 @@ yarn run grunt functionalTests:ensureAllTestsInCiGroup;
if [[ -z "$CODE_COVERAGE" ]] ; then
echo " -> building and extracting OSS Kibana distributable for use in functional tests"
node scripts/build --debug --oss
-
- mkdir -p "$WORKSPACE/kibana-build-oss"
- cp -pR build/oss/kibana-*-SNAPSHOT-linux-x86_64/. $WORKSPACE/kibana-build-oss/
fi
diff --git a/test/scripts/jenkins_build_plugins.sh b/test/scripts/jenkins_build_plugins.sh
deleted file mode 100755
index 32b3942074b34..0000000000000
--- a/test/scripts/jenkins_build_plugins.sh
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-echo " -> building examples separate from test plugins"
-node scripts/build_kibana_platform_plugins \
- --oss \
- --examples \
- --workers 6 \
- --verbose
-
-echo " -> building kibana platform plugins"
-node scripts/build_kibana_platform_plugins \
- --oss \
- --no-examples \
- --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \
- --scan-dir "$KIBANA_DIR/test/interpreter_functional/plugins" \
- --workers 6 \
- --verbose
diff --git a/test/scripts/jenkins_ci_group.sh b/test/scripts/jenkins_ci_group.sh
index 2542d7032e83b..60d7f0406f4c9 100755
--- a/test/scripts/jenkins_ci_group.sh
+++ b/test/scripts/jenkins_ci_group.sh
@@ -5,7 +5,7 @@ source test/scripts/jenkins_test_setup_oss.sh
if [[ -z "$CODE_COVERAGE" ]]; then
checks-reporter-with-killswitch "Functional tests / Group ${CI_GROUP}" yarn run grunt "run:functionalTests_ciGroup${CI_GROUP}";
- if [[ ! "$TASK_QUEUE_PROCESS_ID" && "$CI_GROUP" == "1" ]]; then
+ if [ "$CI_GROUP" == "1" ]; then
source test/scripts/jenkins_build_kbn_sample_panel_action.sh
yarn run grunt run:pluginFunctionalTestsRelease --from=source;
yarn run grunt run:exampleFunctionalTestsRelease --from=source;
diff --git a/test/scripts/jenkins_plugin_functional.sh b/test/scripts/jenkins_plugin_functional.sh
deleted file mode 100755
index 1d691d98982de..0000000000000
--- a/test/scripts/jenkins_plugin_functional.sh
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/usr/bin/env bash
-
-source test/scripts/jenkins_test_setup_oss.sh
-
-cd test/plugin_functional/plugins/kbn_sample_panel_action;
-if [[ ! -d "target" ]]; then
- yarn build;
-fi
-cd -;
-
-pwd
-
-yarn run grunt run:pluginFunctionalTestsRelease --from=source;
-yarn run grunt run:exampleFunctionalTestsRelease --from=source;
-yarn run grunt run:interpreterFunctionalTestsRelease;
diff --git a/test/scripts/jenkins_security_solution_cypress.sh b/test/scripts/jenkins_security_solution_cypress.sh
old mode 100755
new mode 100644
index a5a1a2103801f..204911a3eedaa
--- a/test/scripts/jenkins_security_solution_cypress.sh
+++ b/test/scripts/jenkins_security_solution_cypress.sh
@@ -1,6 +1,12 @@
#!/usr/bin/env bash
-source test/scripts/jenkins_test_setup_xpack.sh
+source test/scripts/jenkins_test_setup.sh
+
+installDir="$PARENT_DIR/install/kibana"
+destDir="${installDir}-${CI_WORKER_NUMBER}"
+cp -R "$installDir" "$destDir"
+
+export KIBANA_INSTALL_DIR="$destDir"
echo " -> Running security solution cypress tests"
cd "$XPACK_DIR"
diff --git a/test/scripts/jenkins_setup_parallel_workspace.sh b/test/scripts/jenkins_setup_parallel_workspace.sh
deleted file mode 100755
index 5274d05572e71..0000000000000
--- a/test/scripts/jenkins_setup_parallel_workspace.sh
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/env bash
-set -e
-
-CURRENT_DIR=$(pwd)
-
-# Copy everything except node_modules into the current workspace
-rsync -a ${WORKSPACE}/kibana/* . --exclude node_modules
-rsync -a ${WORKSPACE}/kibana/.??* .
-
-# Symlink all non-root, non-fixture node_modules into our new workspace
-cd ${WORKSPACE}/kibana
-find . -type d -name node_modules -not -path '*__fixtures__*' -not -path './node_modules*' -prune -print0 | xargs -0I % ln -s "${WORKSPACE}/kibana/%" "${CURRENT_DIR}/%"
-find . -type d -wholename '*__fixtures__*node_modules' -not -path './node_modules*' -prune -print0 | xargs -0I % cp -R "${WORKSPACE}/kibana/%" "${CURRENT_DIR}/%"
-cd "${CURRENT_DIR}"
-
-# Symlink all of the individual root-level node_modules into the node_modules/ directory
-mkdir -p node_modules
-ln -s ${WORKSPACE}/kibana/node_modules/* node_modules/
-ln -s ${WORKSPACE}/kibana/node_modules/.??* node_modules/
-
-# Copy a few node_modules instead of symlinking them. They don't work correctly if symlinked
-unlink node_modules/@kbn
-unlink node_modules/css-loader
-unlink node_modules/style-loader
-
-# packages/kbn-optimizer/src/integration_tests/basic_optimization.test.ts will fail if this is a symlink
-unlink node_modules/val-loader
-
-cp -R ${WORKSPACE}/kibana/node_modules/@kbn node_modules/
-cp -R ${WORKSPACE}/kibana/node_modules/css-loader node_modules/
-cp -R ${WORKSPACE}/kibana/node_modules/style-loader node_modules/
-cp -R ${WORKSPACE}/kibana/node_modules/val-loader node_modules/
diff --git a/test/scripts/jenkins_test_setup.sh b/test/scripts/jenkins_test_setup.sh
old mode 100755
new mode 100644
index 7cced76eb650f..49ee8a6b526ca
--- a/test/scripts/jenkins_test_setup.sh
+++ b/test/scripts/jenkins_test_setup.sh
@@ -14,7 +14,3 @@ trap 'post_work' EXIT
export TEST_BROWSER_HEADLESS=1
source src/dev/ci_setup/setup_env.sh
-
-if [[ ! -d .es && -d "$WORKSPACE/kibana/.es" ]]; then
- cp -R $WORKSPACE/kibana/.es ./
-fi
diff --git a/test/scripts/jenkins_test_setup_oss.sh b/test/scripts/jenkins_test_setup_oss.sh
old mode 100755
new mode 100644
index b7eac33f35176..7bbb867526384
--- a/test/scripts/jenkins_test_setup_oss.sh
+++ b/test/scripts/jenkins_test_setup_oss.sh
@@ -2,17 +2,10 @@
source test/scripts/jenkins_test_setup.sh
-if [[ -z "$CODE_COVERAGE" ]]; then
-
- destDir="build/kibana-build-oss"
- if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then
- destDir="${destDir}-${CI_PARALLEL_PROCESS_NUMBER}"
- fi
-
- if [[ ! -d $destDir ]]; then
- mkdir -p $destDir
- cp -pR "$WORKSPACE/kibana-build-oss/." $destDir/
- fi
+if [[ -z "$CODE_COVERAGE" ]] ; then
+ installDir="$(realpath $PARENT_DIR/kibana/build/oss/kibana-*-SNAPSHOT-linux-x86_64)"
+ destDir=${installDir}-${CI_PARALLEL_PROCESS_NUMBER}
+ cp -R "$installDir" "$destDir"
export KIBANA_INSTALL_DIR="$destDir"
fi
diff --git a/test/scripts/jenkins_test_setup_xpack.sh b/test/scripts/jenkins_test_setup_xpack.sh
old mode 100755
new mode 100644
index 74a3de77e3a76..a72e9749ebbd5
--- a/test/scripts/jenkins_test_setup_xpack.sh
+++ b/test/scripts/jenkins_test_setup_xpack.sh
@@ -3,18 +3,11 @@
source test/scripts/jenkins_test_setup.sh
if [[ -z "$CODE_COVERAGE" ]]; then
+ installDir="$PARENT_DIR/install/kibana"
+ destDir="${installDir}-${CI_PARALLEL_PROCESS_NUMBER}"
+ cp -R "$installDir" "$destDir"
- destDir="build/kibana-build-xpack"
- if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then
- destDir="${destDir}-${CI_PARALLEL_PROCESS_NUMBER}"
- fi
-
- if [[ ! -d $destDir ]]; then
- mkdir -p $destDir
- cp -pR "$WORKSPACE/kibana-build-xpack/." $destDir/
- fi
-
- export KIBANA_INSTALL_DIR="$(realpath $destDir)"
+ export KIBANA_INSTALL_DIR="$destDir"
cd "$XPACK_DIR"
fi
diff --git a/test/scripts/jenkins_xpack_build_kibana.sh b/test/scripts/jenkins_xpack_build_kibana.sh
index 2452e2f5b8c58..c962b962b1e5e 100755
--- a/test/scripts/jenkins_xpack_build_kibana.sh
+++ b/test/scripts/jenkins_xpack_build_kibana.sh
@@ -3,9 +3,15 @@
cd "$KIBANA_DIR"
source src/dev/ci_setup/setup_env.sh
-if [[ ! "$TASK_QUEUE_PROCESS_ID" ]]; then
- ./test/scripts/jenkins_xpack_build_plugins.sh
-fi
+echo " -> building kibana platform plugins"
+node scripts/build_kibana_platform_plugins \
+ --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \
+ --scan-dir "$XPACK_DIR/test/plugin_functional/plugins" \
+ --scan-dir "$XPACK_DIR/test/functional_with_es_ssl/fixtures/plugins" \
+ --scan-dir "$XPACK_DIR/test/alerting_api_integration/plugins" \
+ --scan-dir "$XPACK_DIR/test/plugin_api_integration/plugins" \
+ --scan-dir "$XPACK_DIR/test/plugin_api_perf/plugins" \
+ --verbose;
# doesn't persist, also set in kibanaPipeline.groovy
export KBN_NP_PLUGINS_BUILT=true
@@ -30,10 +36,7 @@ if [[ -z "$CODE_COVERAGE" ]] ; then
cd "$KIBANA_DIR"
node scripts/build --debug --no-oss
linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')"
- installDir="$KIBANA_DIR/install/kibana"
+ installDir="$PARENT_DIR/install/kibana"
mkdir -p "$installDir"
tar -xzf "$linuxBuild" -C "$installDir" --strip=1
-
- mkdir -p "$WORKSPACE/kibana-build-xpack"
- cp -pR install/kibana/. $WORKSPACE/kibana-build-xpack/
fi
diff --git a/test/scripts/jenkins_xpack_build_plugins.sh b/test/scripts/jenkins_xpack_build_plugins.sh
deleted file mode 100755
index fea30c547bd5f..0000000000000
--- a/test/scripts/jenkins_xpack_build_plugins.sh
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-echo " -> building examples separate from test plugins"
-node scripts/build_kibana_platform_plugins \
- --workers 12 \
- --examples \
- --verbose
-
-echo " -> building kibana platform plugins"
-node scripts/build_kibana_platform_plugins \
- --no-examples \
- --scan-dir "$KIBANA_DIR/test/plugin_functional/plugins" \
- --scan-dir "$XPACK_DIR/test/plugin_functional/plugins" \
- --scan-dir "$XPACK_DIR/test/functional_with_es_ssl/fixtures/plugins" \
- --scan-dir "$XPACK_DIR/test/alerting_api_integration/plugins" \
- --scan-dir "$XPACK_DIR/test/plugin_api_integration/plugins" \
- --scan-dir "$XPACK_DIR/test/plugin_api_perf/plugins" \
- --workers 12 \
- --verbose
diff --git a/test/scripts/jenkins_xpack_page_load_metrics.sh b/test/scripts/jenkins_xpack_page_load_metrics.sh
old mode 100755
new mode 100644
diff --git a/test/scripts/jenkins_xpack_visual_regression.sh b/test/scripts/jenkins_xpack_visual_regression.sh
index 930d4a74345d9..ac567a188a6d4 100755
--- a/test/scripts/jenkins_xpack_visual_regression.sh
+++ b/test/scripts/jenkins_xpack_visual_regression.sh
@@ -5,7 +5,7 @@ source "$KIBANA_DIR/src/dev/ci_setup/setup_percy.sh"
echo " -> building and extracting default Kibana distributable"
cd "$KIBANA_DIR"
-node scripts/build --debug
+node scripts/build --debug --no-oss
linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')"
installDir="$PARENT_DIR/install/kibana"
mkdir -p "$installDir"
@@ -22,5 +22,5 @@ yarn percy exec -t 10000 -- -- \
# cd "$KIBANA_DIR"
# source "test/scripts/jenkins_xpack_page_load_metrics.sh"
-cd "$XPACK_DIR"
-source "$KIBANA_DIR/test/scripts/jenkins_xpack_saved_objects_field_metrics.sh"
+cd "$KIBANA_DIR"
+source "test/scripts/jenkins_xpack_saved_objects_field_metrics.sh"
diff --git a/test/scripts/lint/eslint.sh b/test/scripts/lint/eslint.sh
deleted file mode 100755
index c3211300b96c5..0000000000000
--- a/test/scripts/lint/eslint.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:eslint
diff --git a/test/scripts/lint/sasslint.sh b/test/scripts/lint/sasslint.sh
deleted file mode 100755
index b9c683bcb049e..0000000000000
--- a/test/scripts/lint/sasslint.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:sasslint
diff --git a/test/scripts/test/api_integration.sh b/test/scripts/test/api_integration.sh
deleted file mode 100755
index 152c97a3ca7df..0000000000000
--- a/test/scripts/test/api_integration.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:apiIntegrationTests
diff --git a/test/scripts/test/jest_integration.sh b/test/scripts/test/jest_integration.sh
deleted file mode 100755
index 73dbbddfb38f6..0000000000000
--- a/test/scripts/test/jest_integration.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:test_jest_integration
diff --git a/test/scripts/test/jest_unit.sh b/test/scripts/test/jest_unit.sh
deleted file mode 100755
index e25452698cebc..0000000000000
--- a/test/scripts/test/jest_unit.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:test_jest
diff --git a/test/scripts/test/karma_ci.sh b/test/scripts/test/karma_ci.sh
deleted file mode 100755
index e9985300ba19d..0000000000000
--- a/test/scripts/test/karma_ci.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:test_karma_ci
diff --git a/test/scripts/test/mocha.sh b/test/scripts/test/mocha.sh
deleted file mode 100755
index 43c00f0a09dcf..0000000000000
--- a/test/scripts/test/mocha.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-yarn run grunt run:mocha
diff --git a/test/scripts/test/xpack_jest_unit.sh b/test/scripts/test/xpack_jest_unit.sh
deleted file mode 100755
index 93d70ec355391..0000000000000
--- a/test/scripts/test/xpack_jest_unit.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-cd x-pack
-checks-reporter-with-killswitch "X-Pack Jest" node --max-old-space-size=6144 scripts/jest --ci --verbose --maxWorkers=10
diff --git a/test/scripts/test/xpack_karma.sh b/test/scripts/test/xpack_karma.sh
deleted file mode 100755
index 9078f01f1b870..0000000000000
--- a/test/scripts/test/xpack_karma.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-cd x-pack
-checks-reporter-with-killswitch "X-Pack Karma Tests" yarn test:karma
diff --git a/test/scripts/test/xpack_list_cyclic_dependency.sh b/test/scripts/test/xpack_list_cyclic_dependency.sh
deleted file mode 100755
index 493fe9f58d322..0000000000000
--- a/test/scripts/test/xpack_list_cyclic_dependency.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-cd x-pack
-checks-reporter-with-killswitch "X-Pack List cyclic dependency test" node plugins/lists/scripts/check_circular_deps
diff --git a/test/scripts/test/xpack_siem_cyclic_dependency.sh b/test/scripts/test/xpack_siem_cyclic_dependency.sh
deleted file mode 100755
index b21301f25ad08..0000000000000
--- a/test/scripts/test/xpack_siem_cyclic_dependency.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env bash
-
-source src/dev/ci_setup/setup_env.sh
-
-cd x-pack
-checks-reporter-with-killswitch "X-Pack SIEM cyclic dependency test" node plugins/security_solution/scripts/check_circular_deps
diff --git a/vars/catchErrors.groovy b/vars/catchErrors.groovy
index 2a1b55d832606..460a90b8ec0c0 100644
--- a/vars/catchErrors.groovy
+++ b/vars/catchErrors.groovy
@@ -1,15 +1,8 @@
// Basically, this is a shortcut for catchError(catchInterruptions: false) {}
// By default, catchError will swallow aborts/timeouts, which we almost never want
-// Also, by wrapping it in an additional try/catch, we cut down on spam in Pipeline Steps
def call(Map params = [:], Closure closure) {
- try {
- closure()
- } catch (ex) {
- params.catchInterruptions = false
- catchError(params) {
- throw ex
- }
- }
+ params.catchInterruptions = false
+ return catchError(params, closure)
}
return this
diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy
index 0f11204311451..f3fc5f84583c9 100644
--- a/vars/kibanaPipeline.groovy
+++ b/vars/kibanaPipeline.groovy
@@ -16,34 +16,27 @@ def withPostBuildReporting(Closure closure) {
}
}
-def withFunctionalTestEnv(List additionalEnvs = [], Closure closure) {
- // This can go away once everything that uses the deprecated workers.parallelProcesses() is moved to task queue
- def parallelId = env.TASK_QUEUE_PROCESS_ID ?: env.CI_PARALLEL_PROCESS_NUMBER
-
- def kibanaPort = "61${parallelId}1"
- def esPort = "61${parallelId}2"
- def esTransportPort = "61${parallelId}3"
- def ingestManagementPackageRegistryPort = "61${parallelId}4"
-
- withEnv([
- "CI_GROUP=${parallelId}",
- "REMOVE_KIBANA_INSTALL_DIR=1",
- "CI_PARALLEL_PROCESS_NUMBER=${parallelId}",
- "TEST_KIBANA_HOST=localhost",
- "TEST_KIBANA_PORT=${kibanaPort}",
- "TEST_KIBANA_URL=http://elastic:changeme@localhost:${kibanaPort}",
- "TEST_ES_URL=http://elastic:changeme@localhost:${esPort}",
- "TEST_ES_TRANSPORT_PORT=${esTransportPort}",
- "KBN_NP_PLUGINS_BUILT=true",
- "INGEST_MANAGEMENT_PACKAGE_REGISTRY_PORT=${ingestManagementPackageRegistryPort}",
- ] + additionalEnvs) {
- closure()
- }
-}
-
def functionalTestProcess(String name, Closure closure) {
- return {
- withFunctionalTestEnv(["JOB=${name}"], closure)
+ return { processNumber ->
+ def kibanaPort = "61${processNumber}1"
+ def esPort = "61${processNumber}2"
+ def esTransportPort = "61${processNumber}3"
+ def ingestManagementPackageRegistryPort = "61${processNumber}4"
+
+ withEnv([
+ "CI_PARALLEL_PROCESS_NUMBER=${processNumber}",
+ "TEST_KIBANA_HOST=localhost",
+ "TEST_KIBANA_PORT=${kibanaPort}",
+ "TEST_KIBANA_URL=http://elastic:changeme@localhost:${kibanaPort}",
+ "TEST_ES_URL=http://elastic:changeme@localhost:${esPort}",
+ "TEST_ES_TRANSPORT_PORT=${esTransportPort}",
+ "INGEST_MANAGEMENT_PACKAGE_REGISTRY_PORT=${ingestManagementPackageRegistryPort}",
+ "IS_PIPELINE_JOB=1",
+ "JOB=${name}",
+ "KBN_NP_PLUGINS_BUILT=true",
+ ]) {
+ closure()
+ }
}
}
@@ -107,17 +100,11 @@ def withGcsArtifactUpload(workerName, closure) {
def uploadPrefix = "kibana-ci-artifacts/jobs/${env.JOB_NAME}/${BUILD_NUMBER}/${workerName}"
def ARTIFACT_PATTERNS = [
'target/kibana-*',
- 'target/test-metrics/*',
'target/kibana-security-solution/**/*.png',
'target/junit/**/*',
- 'target/test-suites-ci-plan.json',
- 'test/**/screenshots/session/*.png',
- 'test/**/screenshots/failure/*.png',
- 'test/**/screenshots/diff/*.png',
+ 'test/**/screenshots/**/*.png',
'test/functional/failure_debug/html/*.html',
- 'x-pack/test/**/screenshots/session/*.png',
- 'x-pack/test/**/screenshots/failure/*.png',
- 'x-pack/test/**/screenshots/diff/*.png',
+ 'x-pack/test/**/screenshots/**/*.png',
'x-pack/test/functional/failure_debug/html/*.html',
'x-pack/test/functional/apps/reporting/reports/session/*.pdf',
]
@@ -132,12 +119,6 @@ def withGcsArtifactUpload(workerName, closure) {
ARTIFACT_PATTERNS.each { pattern ->
uploadGcsArtifact(uploadPrefix, pattern)
}
-
- dir(env.WORKSPACE) {
- ARTIFACT_PATTERNS.each { pattern ->
- uploadGcsArtifact(uploadPrefix, "parallel/*/kibana/${pattern}")
- }
- }
}
}
})
@@ -150,11 +131,6 @@ def withGcsArtifactUpload(workerName, closure) {
def publishJunit() {
junit(testResults: 'target/junit/**/*.xml', allowEmptyResults: true, keepLongStdio: true)
-
- // junit() is weird about paths for security reasons, so we need to actually change to an upper directory first
- dir(env.WORKSPACE) {
- junit(testResults: 'parallel/*/kibana/target/junit/**/*.xml', allowEmptyResults: true, keepLongStdio: true)
- }
}
def sendMail() {
@@ -218,16 +194,12 @@ def doSetup() {
}
}
-def buildOss(maxWorkers = '') {
- withEnv(["KBN_OPTIMIZER_MAX_WORKERS=${maxWorkers}"]) {
- runbld("./test/scripts/jenkins_build_kibana.sh", "Build OSS/Default Kibana")
- }
+def buildOss() {
+ runbld("./test/scripts/jenkins_build_kibana.sh", "Build OSS/Default Kibana")
}
-def buildXpack(maxWorkers = '') {
- withEnv(["KBN_OPTIMIZER_MAX_WORKERS=${maxWorkers}"]) {
- runbld("./test/scripts/jenkins_xpack_build_kibana.sh", "Build X-Pack Kibana")
- }
+def buildXpack() {
+ runbld("./test/scripts/jenkins_xpack_build_kibana.sh", "Build X-Pack Kibana")
}
def runErrorReporter() {
@@ -276,100 +248,6 @@ def call(Map params = [:], Closure closure) {
}
}
-// Creates a task queue using withTaskQueue, and copies the bootstrapped kibana repo into each process's workspace
-// Note that node_modules are mostly symlinked to save time/space. See test/scripts/jenkins_setup_parallel_workspace.sh
-def withCiTaskQueue(Map options = [:], Closure closure) {
- def setupClosure = {
- // This can't use runbld, because it expects the source to be there, which isn't yet
- bash("${env.WORKSPACE}/kibana/test/scripts/jenkins_setup_parallel_workspace.sh", "Set up duplicate workspace for parallel process")
- }
-
- def config = [parallel: 24, setup: setupClosure] + options
-
- withTaskQueue(config) {
- closure.call()
- }
-}
-
-def scriptTask(description, script) {
- return {
- withFunctionalTestEnv {
- runbld(script, description)
- }
- }
-}
-
-def scriptTaskDocker(description, script) {
- return {
- withDocker(scriptTask(description, script))
- }
-}
-
-def buildDocker() {
- sh(
- script: """
- cp /usr/local/bin/runbld .ci/
- cp /usr/local/bin/bash_standard_lib.sh .ci/
- cd .ci
- docker build -t kibana-ci -f ./Dockerfile .
- """,
- label: 'Build CI Docker image'
- )
-}
-
-def withDocker(Closure closure) {
- docker
- .image('kibana-ci')
- .inside(
- "-v /etc/runbld:/etc/runbld:ro -v '${env.JENKINS_HOME}:${env.JENKINS_HOME}' -v '/dev/shm/workspace:/dev/shm/workspace' --shm-size 2GB --cpus 4",
- closure
- )
-}
-
-def buildOssPlugins() {
- runbld('./test/scripts/jenkins_build_plugins.sh', 'Build OSS Plugins')
-}
-
-def buildXpackPlugins() {
- runbld('./test/scripts/jenkins_xpack_build_plugins.sh', 'Build X-Pack Plugins')
-}
-
-def withTasks(Map params = [worker: [:]], Closure closure) {
- catchErrors {
- def config = [name: 'ci-worker', size: 'xxl', ramDisk: true] + (params.worker ?: [:])
-
- workers.ci(config) {
- withCiTaskQueue(parallel: 24) {
- parallel([
- docker: {
- retry(2) {
- buildDocker()
- }
- },
-
- // There are integration tests etc that require the plugins to be built first, so let's go ahead and build them before set up the parallel workspaces
- ossPlugins: { buildOssPlugins() },
- xpackPlugins: { buildXpackPlugins() },
- ])
-
- catchErrors {
- closure()
- }
- }
- }
- }
-}
-
-def allCiTasks() {
- withTasks {
- tasks.check()
- tasks.lint()
- tasks.test()
- tasks.functionalOss()
- tasks.functionalXpack()
- }
-}
-
def pipelineLibraryTests() {
whenChanged(['vars/', '.ci/pipeline-library/']) {
workers.base(size: 'flyweight', bootstrapped: false, ramDisk: false) {
@@ -380,4 +258,5 @@ def pipelineLibraryTests() {
}
}
+
return this
diff --git a/vars/task.groovy b/vars/task.groovy
deleted file mode 100644
index 0c07b519b6fef..0000000000000
--- a/vars/task.groovy
+++ /dev/null
@@ -1,5 +0,0 @@
-def call(Closure closure) {
- withTaskQueue.addTask(closure)
-}
-
-return this
diff --git a/vars/tasks.groovy b/vars/tasks.groovy
deleted file mode 100644
index 9de4c78322d3e..0000000000000
--- a/vars/tasks.groovy
+++ /dev/null
@@ -1,118 +0,0 @@
-def call(List closures) {
- withTaskQueue.addTasks(closures)
-}
-
-def check() {
- tasks([
- kibanaPipeline.scriptTask('Check TypeScript Projects', 'test/scripts/checks/ts_projects.sh'),
- kibanaPipeline.scriptTask('Check Doc API Changes', 'test/scripts/checks/doc_api_changes.sh'),
- kibanaPipeline.scriptTask('Check Types', 'test/scripts/checks/type_check.sh'),
- kibanaPipeline.scriptTask('Check i18n', 'test/scripts/checks/i18n.sh'),
- kibanaPipeline.scriptTask('Check File Casing', 'test/scripts/checks/file_casing.sh'),
- kibanaPipeline.scriptTask('Check Lockfile Symlinks', 'test/scripts/checks/lock_file_symlinks.sh'),
- kibanaPipeline.scriptTask('Check Licenses', 'test/scripts/checks/licenses.sh'),
- kibanaPipeline.scriptTask('Verify Dependency Versions', 'test/scripts/checks/verify_dependency_versions.sh'),
- kibanaPipeline.scriptTask('Verify NOTICE', 'test/scripts/checks/verify_notice.sh'),
- kibanaPipeline.scriptTask('Test Projects', 'test/scripts/checks/test_projects.sh'),
- kibanaPipeline.scriptTask('Test Hardening', 'test/scripts/checks/test_hardening.sh'),
- ])
-}
-
-def lint() {
- tasks([
- kibanaPipeline.scriptTask('Lint: eslint', 'test/scripts/lint/eslint.sh'),
- kibanaPipeline.scriptTask('Lint: sasslint', 'test/scripts/lint/sasslint.sh'),
- ])
-}
-
-def test() {
- tasks([
- // These 4 tasks require isolation because of hard-coded, conflicting ports and such, so let's use Docker here
- kibanaPipeline.scriptTaskDocker('Jest Integration Tests', 'test/scripts/test/jest_integration.sh'),
- kibanaPipeline.scriptTaskDocker('Mocha Tests', 'test/scripts/test/mocha.sh'),
- kibanaPipeline.scriptTaskDocker('Karma CI Tests', 'test/scripts/test/karma_ci.sh'),
- kibanaPipeline.scriptTaskDocker('X-Pack Karma Tests', 'test/scripts/test/xpack_karma.sh'),
-
- kibanaPipeline.scriptTask('Jest Unit Tests', 'test/scripts/test/jest_unit.sh'),
- kibanaPipeline.scriptTask('API Integration Tests', 'test/scripts/test/api_integration.sh'),
- kibanaPipeline.scriptTask('X-Pack SIEM cyclic dependency', 'test/scripts/test/xpack_siem_cyclic_dependency.sh'),
- kibanaPipeline.scriptTask('X-Pack List cyclic dependency', 'test/scripts/test/xpack_list_cyclic_dependency.sh'),
- kibanaPipeline.scriptTask('X-Pack Jest Unit Tests', 'test/scripts/test/xpack_jest_unit.sh'),
- ])
-}
-
-def functionalOss(Map params = [:]) {
- def config = params ?: [ciGroups: true, firefox: true, accessibility: true, pluginFunctional: true, visualRegression: false]
-
- task {
- kibanaPipeline.buildOss(6)
-
- if (config.ciGroups) {
- def ciGroups = 1..12
- tasks(ciGroups.collect { kibanaPipeline.ossCiGroupProcess(it) })
- }
-
- if (config.firefox) {
- task(kibanaPipeline.functionalTestProcess('oss-firefox', './test/scripts/jenkins_firefox_smoke.sh'))
- }
-
- if (config.accessibility) {
- task(kibanaPipeline.functionalTestProcess('oss-accessibility', './test/scripts/jenkins_accessibility.sh'))
- }
-
- if (config.pluginFunctional) {
- task(kibanaPipeline.functionalTestProcess('oss-pluginFunctional', './test/scripts/jenkins_plugin_functional.sh'))
- }
-
- if (config.visualRegression) {
- task(kibanaPipeline.functionalTestProcess('oss-visualRegression', './test/scripts/jenkins_visual_regression.sh'))
- }
- }
-}
-
-def functionalXpack(Map params = [:]) {
- def config = params ?: [
- ciGroups: true,
- firefox: true,
- accessibility: true,
- pluginFunctional: true,
- savedObjectsFieldMetrics: true,
- pageLoadMetrics: false,
- visualRegression: false,
- ]
-
- task {
- kibanaPipeline.buildXpack(10)
-
- if (config.ciGroups) {
- def ciGroups = 1..10
- tasks(ciGroups.collect { kibanaPipeline.xpackCiGroupProcess(it) })
- }
-
- if (config.firefox) {
- task(kibanaPipeline.functionalTestProcess('xpack-firefox', './test/scripts/jenkins_xpack_firefox_smoke.sh'))
- }
-
- if (config.accessibility) {
- task(kibanaPipeline.functionalTestProcess('xpack-accessibility', './test/scripts/jenkins_xpack_accessibility.sh'))
- }
-
- if (config.visualRegression) {
- task(kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh'))
- }
-
- if (config.pageLoadMetrics) {
- task(kibanaPipeline.functionalTestProcess('xpack-pageLoadMetrics', './test/scripts/jenkins_xpack_page_load_metrics.sh'))
- }
-
- if (config.savedObjectsFieldMetrics) {
- task(kibanaPipeline.functionalTestProcess('xpack-savedObjectsFieldMetrics', './test/scripts/jenkins_xpack_saved_objects_field_metrics.sh'))
- }
-
- whenChanged(['x-pack/plugins/security_solution/', 'x-pack/test/security_solution_cypress/']) {
- task(kibanaPipeline.functionalTestProcess('xpack-securitySolutionCypress', './test/scripts/jenkins_security_solution_cypress.sh'))
- }
- }
-}
-
-return this
diff --git a/vars/withTaskQueue.groovy b/vars/withTaskQueue.groovy
deleted file mode 100644
index 8132d6264744f..0000000000000
--- a/vars/withTaskQueue.groovy
+++ /dev/null
@@ -1,154 +0,0 @@
-import groovy.transform.Field
-
-public static @Field TASK_QUEUES = [:]
-public static @Field TASK_QUEUES_COUNTER = 0
-
-/**
- withTaskQueue creates a queue of "tasks" (just plain closures to execute), and executes them with your desired level of concurrency.
- This way, you can define, for example, 40 things that need to execute, then only allow 10 of them to execute at once.
-
- Each "process" will execute in a separate, unique, empty directory.
- If you want each process to have a bootstrapped kibana repo, check out kibanaPipeline.withCiTaskQueue
-
- Using the queue currently requires an agent/worker.
-
- Usage:
-
- withTaskQueue(parallel: 10) {
- task { print "This is a task" }
-
- // This is the same as calling task() multiple times
- tasks([ { print "Another task" }, { print "And another task" } ])
-
- // Tasks can queue up subsequent tasks
- task {
- buildThing()
- task { print "I depend on buildThing()" }
- }
- }
-
- You can also define a setup task that each process should execute one time before executing tasks:
- withTaskQueue(parallel: 10, setup: { sh "my-setup-scrupt.sh" }) {
- ...
- }
-
-*/
-def call(Map options = [:], Closure closure) {
- def config = [ parallel: 10 ] + options
- def counter = ++TASK_QUEUES_COUNTER
-
- // We're basically abusing withEnv() to create a "scope" for all steps inside of a withTaskQueue block
- // This way, we could have multiple task queue instances in the same pipeline
- withEnv(["TASK_QUEUE_ID=${counter}"]) {
- withTaskQueue.TASK_QUEUES[env.TASK_QUEUE_ID] = [
- tasks: [],
- tmpFile: sh(script: 'mktemp', returnStdout: true).trim()
- ]
-
- closure.call()
-
- def processesExecuting = 0
- def processes = [:]
- def iterationId = 0
-
- for(def i = 1; i <= config.parallel; i++) {
- def j = i
- processes["task-queue-process-${j}"] = {
- catchErrors {
- withEnv([
- "TASK_QUEUE_PROCESS_ID=${j}",
- "TASK_QUEUE_ITERATION_ID=${++iterationId}"
- ]) {
- dir("${WORKSPACE}/parallel/${j}/kibana") {
- if (config.setup) {
- config.setup.call(j)
- }
-
- def isDone = false
- while(!isDone) { // TODO some kind of timeout?
- catchErrors {
- if (!getTasks().isEmpty()) {
- processesExecuting++
- catchErrors {
- def task
- try {
- task = getTasks().pop()
- } catch (java.util.NoSuchElementException ex) {
- return
- }
-
- task.call()
- }
- processesExecuting--
- // If a task finishes, and no new tasks were queued up, and nothing else is executing
- // Then all of the processes should wake up and exit
- if (processesExecuting < 1 && getTasks().isEmpty()) {
- taskNotify()
- }
- return
- }
-
- if (processesExecuting > 0) {
- taskSleep()
- return
- }
-
- // Queue is empty, no processes are executing
- isDone = true
- }
- }
- }
- }
- }
- }
- }
- parallel(processes)
- }
-}
-
-// If we sleep in a loop using Groovy code, Pipeline Steps is flooded with Sleep steps
-// So, instead, we just watch a file and `touch` it whenever something happens that could modify the queue
-// There's a 20 minute timeout just in case something goes wrong,
-// in which case this method will get called again if the process is actually supposed to be waiting.
-def taskSleep() {
- sh(script: """#!/bin/bash
- TIMESTAMP=\$(date '+%s' -d "0 seconds ago")
- for (( i=1; i<=240; i++ ))
- do
- if [ "\$(stat -c %Y '${getTmpFile()}')" -ge "\$TIMESTAMP" ]
- then
- break
- else
- sleep 5
- if [[ \$i == 240 ]]; then
- echo "Waited for new tasks for 20 minutes, exiting in case something went wrong"
- fi
- fi
- done
- """, label: "Waiting for new tasks...")
-}
-
-// Used to let the task queue processes know that either a new task has been queued up, or work is complete
-def taskNotify() {
- sh "touch '${getTmpFile()}'"
-}
-
-def getTasks() {
- return withTaskQueue.TASK_QUEUES[env.TASK_QUEUE_ID].tasks
-}
-
-def getTmpFile() {
- return withTaskQueue.TASK_QUEUES[env.TASK_QUEUE_ID].tmpFile
-}
-
-def addTask(Closure closure) {
- getTasks() << closure
- taskNotify()
-}
-
-def addTasks(List closures) {
- closures.reverse().each {
- getTasks() << it
- }
- taskNotify()
-}
diff --git a/vars/workers.groovy b/vars/workers.groovy
index 2e94ce12f34c0..8b7e8525a7ce3 100644
--- a/vars/workers.groovy
+++ b/vars/workers.groovy
@@ -13,8 +13,6 @@ def label(size) {
return 'docker && tests-l'
case 'xl':
return 'docker && tests-xl'
- case 'xl-highmem':
- return 'docker && tests-xl-highmem'
case 'xxl':
return 'docker && tests-xxl'
}
@@ -57,11 +55,6 @@ def base(Map params, Closure closure) {
}
}
- sh(
- script: "mkdir -p ${env.WORKSPACE}/tmp",
- label: "Create custom temp directory"
- )
-
def checkoutInfo = [:]
if (config.scm) {
@@ -96,7 +89,6 @@ def base(Map params, Closure closure) {
"PR_AUTHOR=${env.ghprbPullAuthorLogin ?: ''}",
"TEST_BROWSER_HEADLESS=1",
"GIT_BRANCH=${checkoutInfo.branch}",
- "TMPDIR=${env.WORKSPACE}/tmp", // For Chrome and anything else that respects it
]) {
withCredentials([
string(credentialsId: 'vault-addr', variable: 'VAULT_ADDR'),
@@ -175,9 +167,7 @@ def parallelProcesses(Map params) {
sleep(delay)
}
- withEnv(["CI_PARALLEL_PROCESS_NUMBER=${processNumber}"]) {
- processClosure()
- }
+ processClosure(processNumber)
}
}
diff --git a/x-pack/.gitignore b/x-pack/.gitignore
index 68262c4bf734b..e181caf2b1a49 100644
--- a/x-pack/.gitignore
+++ b/x-pack/.gitignore
@@ -8,7 +8,7 @@
/test/reporting/configs/failure_debug/
/legacy/plugins/reporting/.chromium/
/legacy/plugins/reporting/.phantom/
-/plugins/reporting/.chromium/
+/plugins/reporting/chromium/
/plugins/reporting/.phantom/
/.aws-config.json
/.env
diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json
index 596ba17d343c0..d0055008eb9bf 100644
--- a/x-pack/.i18nrc.json
+++ b/x-pack/.i18nrc.json
@@ -16,6 +16,7 @@
"xpack.data": "plugins/data_enhanced",
"xpack.embeddableEnhanced": "plugins/embeddable_enhanced",
"xpack.endpoint": "plugins/endpoint",
+ "xpack.enterpriseSearch": "plugins/enterprise_search",
"xpack.features": "plugins/features",
"xpack.fileUpload": "plugins/file_upload",
"xpack.globalSearch": ["plugins/global_search"],
diff --git a/x-pack/.telemetryrc.json b/x-pack/.telemetryrc.json
index 4da44667e167f..2c16491c1096b 100644
--- a/x-pack/.telemetryrc.json
+++ b/x-pack/.telemetryrc.json
@@ -7,7 +7,6 @@
"plugins/apm/server/lib/apm_telemetry/index.ts",
"plugins/canvas/server/collectors/collector.ts",
"plugins/infra/server/usage/usage_collector.ts",
- "plugins/ingest_manager/server/collectors/register.ts",
"plugins/lens/server/usage/collectors.ts",
"plugins/reporting/server/usage/reporting_usage_collector.ts",
"plugins/maps/server/maps_telemetry/collectors/register.ts"
diff --git a/x-pack/build_chromium/README.md b/x-pack/build_chromium/README.md
index 72e41afc80c95..ce7e110a5f914 100644
--- a/x-pack/build_chromium/README.md
+++ b/x-pack/build_chromium/README.md
@@ -20,7 +20,8 @@ You'll need access to our GCP account, which is where we have two machines provi
Chromium is built via a build tool called "ninja". The build can be configured by specifying build flags either in an "args.gn" file or via commandline args. We have an "args.gn" file per platform:
- mac: darwin/args.gn
-- linux: linux/args.gn
+- linux 64bit: linux-x64/args.gn
+- ARM 64bit: linux-aarch64/args.gn
- windows: windows/args.gn
The various build flags are not well documented. Some are documented [here](https://www.chromium.org/developers/gn-build-configuration). Some, such as `enable_basic_printing = false`, I only found by poking through 3rd party build scripts.
@@ -65,15 +66,16 @@ Create the build folder:
Copy the `x-pack/build-chromium` folder to each. Replace `you@your-machine` with the correct username and VM name:
-- Mac: `cp -r ~/dev/elastic/kibana/x-pack/build_chromium ~/chromium/build_chromium`
-- Linux: `gcloud compute scp --recurse ~/dev/elastic/kibana/x-pack/build_chromium you@your-machine:~/chromium/build_chromium --zone=us-east1-b`
+- Mac: `cp -r x-pack/build_chromium ~/chromium/build_chromium`
+- Linux: `gcloud compute scp --recurse x-pack/build_chromium you@your-machine:~/chromium/ --zone=us-east1-b --project "XXXXXXXX"`
- Windows: Copy the `build_chromium` folder via the RDP GUI into `c:\chromium\build_chromium`
There is an init script for each platform. This downloads and installs the necessary prerequisites, sets environment variables, etc.
-- Mac: `~/chromium/build_chromium/darwin/init.sh`
-- Linux: `~/chromium/build_chromium/linux/init.sh`
-- Windows `c:\chromium\build_chromium\windows\init.bat`
+- Mac x64: `~/chromium/build_chromium/darwin/init.sh`
+- Linux x64: `~/chromium/build_chromium/linux/init.sh`
+- Linux arm64: `~/chromium/build_chromium/linux/init.sh arm64`
+- Windows x64: `c:\chromium\build_chromium\windows\init.bat`
In windows, at least, you will need to do a number of extra steps:
@@ -102,15 +104,16 @@ Note: In Linux, you should run the build command in tmux so that if your ssh ses
To run the build, replace the sha in the following commands with the sha that you wish to build:
-- Mac: `python ~/chromium/build_chromium/build.py 312d84c8ce62810976feda0d3457108a6dfff9e6`
-- Linux: `python ~/chromium/build_chromium/build.py 312d84c8ce62810976feda0d3457108a6dfff9e6`
-- Windows: `python c:\chromium\build_chromium\build.py 312d84c8ce62810976feda0d3457108a6dfff9e6`
+- Mac x64: `python ~/chromium/build_chromium/build.py 312d84c8ce62810976feda0d3457108a6dfff9e6`
+- Linux x64: `python ~/chromium/build_chromium/build.py 312d84c8ce62810976feda0d3457108a6dfff9e6`
+- Linux arm64: `python ~/chromium/build_chromium/build.py 312d84c8ce62810976feda0d3457108a6dfff9e6 arm64`
+- Windows x64: `python c:\chromium\build_chromium\build.py 312d84c8ce62810976feda0d3457108a6dfff9e6`
## Artifacts
-After the build completes, there will be a .zip file and a .md5 file in `~/chromium/chromium/src/out/headless`. These are named like so: `chromium-{first_7_of_SHA}-{platform}`, for example: `chromium-4747cc2-linux`.
+After the build completes, there will be a .zip file and a .md5 file in `~/chromium/chromium/src/out/headless`. These are named like so: `chromium-{first_7_of_SHA}-{platform}-{arch}`, for example: `chromium-4747cc2-linux-x64`.
-The zip files need to be deployed to s3. For testing, I drop them into `headless-shell-dev`, but for production, they need to be in `headless-shell`. And the `x-pack/plugins/reporting/server/browsers/chromium/paths.ts` file needs to be upated to have the correct `archiveChecksum`, `archiveFilename`, `binaryChecksum` and `baseUrl`. Below is a list of what the archive's are:
+The zip files need to be deployed to GCP Storage. For testing, I drop them into `headless-shell-dev`, but for production, they need to be in `headless-shell`. And the `x-pack/plugins/reporting/server/browsers/chromium/paths.ts` file needs to be upated to have the correct `archiveChecksum`, `archiveFilename`, `binaryChecksum` and `baseUrl`. Below is a list of what the archive's are:
- `archiveChecksum`: The contents of the `.md5` file, which is the `md5` checksum of the zip file.
- `binaryChecksum`: The `md5` checksum of the `headless_shell` binary itself.
@@ -139,8 +142,8 @@ In the case of Windows, you can use IE to open `http://localhost:9221` and see i
The following links provide helpful context about how the Chromium build works, and its prerequisites:
- https://www.chromium.org/developers/how-tos/get-the-code/working-with-release-branches
-- https://chromium.googlesource.com/chromium/src/+/master/docs/windows_build_instructions.md
-- https://chromium.googlesource.com/chromium/src/+/master/docs/mac_build_instructions.md
-- https://chromium.googlesource.com/chromium/src/+/master/docs/linux_build_instructions.md
+- https://chromium.googlesource.com/chromium/src/+/HEAD/docs/windows_build_instructions.md
+- https://chromium.googlesource.com/chromium/src/+/HEAD/docs/mac_build_instructions.md
+- https://chromium.googlesource.com/chromium/src/+/HEAD/docs/linux/build_instructions.md
- Some build-flag descriptions: https://www.chromium.org/developers/gn-build-configuration
- The serverless Chromium project was indispensable: https://github.com/adieuadieu/serverless-chrome/blob/b29445aa5a96d031be2edd5d1fc8651683bf262c/packages/lambda/builds/chromium/build/build.sh
diff --git a/x-pack/build_chromium/build.py b/x-pack/build_chromium/build.py
index 82b0561fdcfe1..52ba325d6f726 100644
--- a/x-pack/build_chromium/build.py
+++ b/x-pack/build_chromium/build.py
@@ -17,7 +17,10 @@
# 4747cc23ae334a57a35ed3c8e6adcdbc8a50d479
source_version = sys.argv[1]
-print('Building Chromium ' + source_version)
+# Set to "arm" to build for ARM on Linux
+arch_name = sys.argv[2] if len(sys.argv) >= 3 else 'x64'
+
+print('Building Chromium ' + source_version + ' for ' + arch_name)
# Set the environment variables required by the build tools
print('Configuring the build environment')
@@ -42,21 +45,29 @@
print('Copying build args: ' + platform_build_args + ' to out/headless/args.gn')
mkdir('out/headless')
shutil.copyfile(platform_build_args, 'out/headless/args.gn')
+
+print('Adding target_cpu to args')
+
+f = open('out/headless/args.gn', 'a')
+f.write('\rtarget_cpu = "' + arch_name + '"')
+f.close()
+
runcmd('gn gen out/headless')
# Build Chromium... this takes *forever* on underpowered VMs
print('Compiling... this will take a while')
runcmd('autoninja -C out/headless headless_shell')
-# Optimize the output on Linux and Mac by stripping inessentials from the binary
-if platform.system() != 'Windows':
+# Optimize the output on Linux x64 and Mac by stripping inessentials from the binary
+# ARM must be cross-compiled from Linux and can not read the ARM binary in order to strip
+if platform.system() != 'Windows' and arch_name != 'arm64':
print('Optimizing headless_shell')
shutil.move('out/headless/headless_shell', 'out/headless/headless_shell_raw')
runcmd('strip -o out/headless/headless_shell out/headless/headless_shell_raw')
# Create the zip and generate the md5 hash using filenames like:
-# chromium-4747cc2-linux.zip
-base_filename = 'out/headless/chromium-' + source_version[:7].strip('.') + '-' + platform.system().lower()
+# chromium-4747cc2-linux_x64.zip
+base_filename = 'out/headless/chromium-' + source_version[:7].strip('.') + '-' + platform.system().lower() + '_' + arch_name
zip_filename = base_filename + '.zip'
md5_filename = base_filename + '.md5'
@@ -66,7 +77,7 @@
def archive_file(name):
"""A little helper function to write individual files to the zip file"""
from_path = os.path.join('out/headless', name)
- to_path = os.path.join('headless_shell-' + platform.system().lower(), name)
+ to_path = os.path.join('headless_shell-' + platform.system().lower() + '_' + arch_name, name)
archive.write(from_path, to_path)
# Each platform has slightly different requirements for what dependencies
@@ -76,6 +87,9 @@ def archive_file(name):
archive_file(os.path.join('swiftshader', 'libEGL.so'))
archive_file(os.path.join('swiftshader', 'libGLESv2.so'))
+ if arch_name == 'arm64':
+ archive_file(os.path.join('swiftshader', 'libEGL.so'))
+
elif platform.system() == 'Windows':
archive_file('headless_shell.exe')
archive_file('dbghelp.dll')
diff --git a/x-pack/build_chromium/init.py b/x-pack/build_chromium/init.py
index a3c5f8dc16fb7..f543922f7653a 100644
--- a/x-pack/build_chromium/init.py
+++ b/x-pack/build_chromium/init.py
@@ -1,4 +1,4 @@
-import os, platform
+import os, platform, sys
from build_util import runcmd, mkdir, md5_file, root_dir, configure_environment
# This is a cross-platform initialization script which should only be run
@@ -29,4 +29,10 @@
# Build Linux deps
if platform.system() == 'Linux':
os.chdir('src')
+
+ if len(sys.argv) >= 2:
+ sysroot_cmd = 'build/linux/sysroot_scripts/install-sysroot.py --arch=' + sys.argv[1]
+ print('Running `' + sysroot_cmd + '`')
+ runcmd(sysroot_cmd)
+
runcmd('build/install-build-deps.sh')
diff --git a/x-pack/build_chromium/linux/init.sh b/x-pack/build_chromium/linux/init.sh
index e259ebded12a1..83cc4a8e5d4d5 100755
--- a/x-pack/build_chromium/linux/init.sh
+++ b/x-pack/build_chromium/linux/init.sh
@@ -10,4 +10,4 @@ fi
# Launch the cross-platform init script using a relative path
# from this script's location.
-python "`dirname "$0"`/../init.py"
+python "`dirname "$0"`/../init.py" $1
diff --git a/x-pack/dev-tools/jest/setup/polyfills.js b/x-pack/dev-tools/jest/setup/polyfills.js
index 822802f3dacb7..a841a3bf9cad0 100644
--- a/x-pack/dev-tools/jest/setup/polyfills.js
+++ b/x-pack/dev-tools/jest/setup/polyfills.js
@@ -21,3 +21,7 @@ require('whatwg-fetch');
if (!global.URL.hasOwnProperty('createObjectURL')) {
Object.defineProperty(global.URL, 'createObjectURL', { value: () => '' });
}
+
+// Will be replaced with a better solution in EUI
+// https://github.com/elastic/eui/issues/3713
+global._isJest = true;
diff --git a/x-pack/examples/ui_actions_enhanced_examples/kibana.json b/x-pack/examples/ui_actions_enhanced_examples/kibana.json
index a1cd895bb3cd6..160352a9afd66 100644
--- a/x-pack/examples/ui_actions_enhanced_examples/kibana.json
+++ b/x-pack/examples/ui_actions_enhanced_examples/kibana.json
@@ -6,5 +6,9 @@
"server": false,
"ui": true,
"requiredPlugins": ["uiActionsEnhanced", "data", "discover"],
- "optionalPlugins": []
+ "optionalPlugins": [],
+ "requiredBundles": [
+ "kibanaUtils",
+ "kibanaReact"
+ ]
}
diff --git a/x-pack/gulpfile.js b/x-pack/gulpfile.js
index 0118d178f54e5..adccaccecd7da 100644
--- a/x-pack/gulpfile.js
+++ b/x-pack/gulpfile.js
@@ -9,13 +9,11 @@ require('../src/setup_node_env');
const { buildTask } = require('./tasks/build');
const { devTask } = require('./tasks/dev');
const { testTask, testKarmaTask, testKarmaDebugTask } = require('./tasks/test');
-const { prepareTask } = require('./tasks/prepare');
// export the tasks that are runnable from the CLI
module.exports = {
build: buildTask,
dev: devTask,
- prepare: prepareTask,
test: testTask,
'test:karma': testKarmaTask,
'test:karma:debug': testKarmaDebugTask,
diff --git a/x-pack/package.json b/x-pack/package.json
index b721cb2fc563a..29264f8920e5d 100644
--- a/x-pack/package.json
+++ b/x-pack/package.json
@@ -196,7 +196,7 @@
"@elastic/apm-rum-react": "^1.1.2",
"@elastic/datemath": "5.0.3",
"@elastic/ems-client": "7.9.3",
- "@elastic/eui": "24.1.0",
+ "@elastic/eui": "26.3.1",
"@elastic/filesaver": "1.1.2",
"@elastic/maki": "6.3.0",
"@elastic/node-crypto": "1.2.1",
diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md
index 494f2f38e8bff..9e07727204f88 100644
--- a/x-pack/plugins/actions/README.md
+++ b/x-pack/plugins/actions/README.md
@@ -26,15 +26,19 @@ Table of Contents
- [Executor](#executor)
- [Example](#example)
- [RESTful API](#restful-api)
- - [`POST /api/actions/action`: Create action](#post-apiaction-create-action)
- - [`DELETE /api/actions/action/{id}`: Delete action](#delete-apiactionid-delete-action)
- - [`GET /api/actions`: Get all actions](#get-apiactiongetall-get-all-actions)
- - [`GET /api/actions/action/{id}`: Get action](#get-apiactionid-get-action)
- - [`GET /api/actions/list_action_types`: List action types](#get-apiactiontypes-list-action-types)
- - [`PUT /api/actions/action/{id}`: Update action](#put-apiactionid-update-action)
- - [`POST /api/actions/action/{id}/_execute`: Execute action](#post-apiactionidexecute-execute-action)
+ - [`POST /api/actions/action`: Create action](#post-apiactionsaction-create-action)
+ - [`DELETE /api/actions/action/{id}`: Delete action](#delete-apiactionsactionid-delete-action)
+ - [`GET /api/actions`: Get all actions](#get-apiactions-get-all-actions)
+ - [`GET /api/actions/action/{id}`: Get action](#get-apiactionsactionid-get-action)
+ - [`GET /api/actions/list_action_types`: List action types](#get-apiactionslist_action_types-list-action-types)
+ - [`PUT /api/actions/action/{id}`: Update action](#put-apiactionsactionid-update-action)
+ - [`POST /api/actions/action/{id}/_execute`: Execute action](#post-apiactionsactionid_execute-execute-action)
- [Firing actions](#firing-actions)
+ - [Accessing a scoped ActionsClient](#accessing-a-scoped-actionsclient)
+ - [actionsClient.enqueueExecution(options)](#actionsclientenqueueexecutionoptions)
- [Example](#example-1)
+ - [actionsClient.execute(options)](#actionsclientexecuteoptions)
+ - [Example](#example-2)
- [Built-in Action Types](#built-in-action-types)
- [Server log](#server-log)
- [`config`](#config)
@@ -70,6 +74,11 @@ Table of Contents
- [`secrets`](#secrets-7)
- [`params`](#params-7)
- [`subActionParams (pushToService)`](#subactionparams-pushtoservice-1)
+ - [IBM Resilient](#ibm-resilient)
+ - [`config`](#config-8)
+ - [`secrets`](#secrets-8)
+ - [`params`](#params-8)
+ - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-2)
- [Command Line Utility](#command-line-utility)
- [Developing New Action Types](#developing-new-action-types)
@@ -99,7 +108,7 @@ Built-In-Actions are configured using the _xpack.actions_ namespoace under _kiba
| _xpack.actions._**enabled** | Feature toggle which enabled Actions in Kibana. | boolean |
| _xpack.actions._**whitelistedHosts** | Which _hostnames_ are whitelisted for the Built-In-Action? This list should contain hostnames of every external service you wish to interact with using Webhooks, Email or any other built in Action. Note that you may use the string "\*" in place of a specific hostname to enable Kibana to target any URL, but keep in mind the potential use of such a feature to execute [SSRF](https://www.owasp.org/index.php/Server_Side_Request_Forgery) attacks from your server. | Array |
| _xpack.actions._**enabledActionTypes** | A list of _actionTypes_ id's that are enabled. A "\*" may be used as an element to indicate all registered actionTypes should be enabled. The actionTypes registered for Kibana are `.server-log`, `.slack`, `.email`, `.index`, `.pagerduty`, `.webhook`. Default: `["*"]` | Array |
-| _xpack.actions._**preconfigured** | A object of action id / preconfigured actions. Default: `{}` | Array |
+| _xpack.actions._**preconfigured** | A object of action id / preconfigured actions. Default: `{}` | Array |
#### Whitelisting Built-in Action Types
@@ -251,6 +260,7 @@ Once you have a scoped ActionsClient you can execute an action by caling either
This api schedules a task which will run the action using the current user scope at the soonest opportunity.
Running the action by scheduling a task means that we will no longer have a user request by which to ascertain the action's privileges and so you might need to provide these yourself:
+
- The **SpaceId** in which the user's action is expected to run
- When security is enabled you'll also need to provide an **apiKey** which allows us to mimic the user and their privileges.
@@ -287,14 +297,14 @@ This api runs the action and asynchronously returns the result of running the ac
The following table describes the properties of the `options` object.
-| Property | Description | Type |
-| -------- | ------------------------------------------------------------------------------------------------------ | ------ |
-| id | The id of the action you want to execute. | string |
-| params | The `params` value to give the action type executor. | object |
+| Property | Description | Type |
+| -------- | ---------------------------------------------------- | ------ |
+| id | The id of the action you want to execute. | string |
+| params | The `params` value to give the action type executor. | object |
## Example
-As with the previous example, we'll use the action `3c5b2bd4-5424-4e4b-8cf5-c0a58c762cc5` to send an email.
+As with the previous example, we'll use the action `3c5b2bd4-5424-4e4b-8cf5-c0a58c762cc5` to send an email.
```typescript
const actionsClient = await server.plugins.actions.getActionsClientWithRequest(request);
@@ -559,10 +569,10 @@ The Jira action uses the [V2 API](https://developer.atlassian.com/cloud/jira/pla
### `config`
-| Property | Description | Type |
-| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ |
-| apiUrl | ServiceNow instance URL. | string |
-| casesConfiguration | Case configuration object. The object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the Jira field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'summary', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in ServiceNow and will be overwrite on each update. | object |
+| Property | Description | Type |
+| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ |
+| apiUrl | Jira instance URL. | string |
+| casesConfiguration | Case configuration object. The object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the Jira field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'summary', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in Jira and will be overwrite on each update. | object |
### `secrets`
@@ -588,6 +598,41 @@ The Jira action uses the [V2 API](https://developer.atlassian.com/cloud/jira/pla
| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ |
| externalId | The id of the incident in Jira. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ |
+## IBM Resilient
+
+ID: `.resilient`
+
+### `config`
+
+| Property | Description | Type |
+| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------ |
+| apiUrl | IBM Resilient instance URL. | string |
+| casesConfiguration | Case configuration object. The object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the Jira field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'summary', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in IBM Resilient and will be overwrite on each update. | object |
+
+### `secrets`
+
+| Property | Description | Type |
+| ------------ | -------------------------------------------- | ------ |
+| apiKeyId | API key ID for HTTP Basic authentication | string |
+| apiKeySecret | API key secret for HTTP Basic authentication | string |
+
+### `params`
+
+| Property | Description | Type |
+| --------------- | ------------------------------------------------------------------------------------ | ------ |
+| subAction | The sub action to perform. It can be `pushToService`, `handshake`, and `getIncident` | string |
+| subActionParams | The parameters of the sub action | object |
+
+#### `subActionParams (pushToService)`
+
+| Property | Description | Type |
+| ----------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------- |
+| caseId | The case id | string |
+| title | The title of the case | string _(optional)_ |
+| description | The description of the case | string _(optional)_ |
+| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ |
+| externalId | The id of the incident in IBM Resilient. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ |
+
# Command Line Utility
The [`kbn-action`](https://github.com/pmuellr/kbn-action) tool can be used to send HTTP requests to the Actions plugin. For instance, to create a Slack action from the `.slack` Action Type, use the following command:
diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts
index dbb18fa5c695c..2e3cee3946d61 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts
@@ -243,7 +243,7 @@ describe('transformFields', () => {
});
});
- test('add newline character to descripton', () => {
+ test('add newline character to description', () => {
const fields = prepareFieldsForTransformation({
externalCase: fullParams.externalCase,
mapping: finalMapping,
diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.ts b/x-pack/plugins/actions/server/builtin_action_types/index.ts
index 0020161789d71..80a171cbe624d 100644
--- a/x-pack/plugins/actions/server/builtin_action_types/index.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/index.ts
@@ -16,6 +16,7 @@ import { getActionType as getSlackActionType } from './slack';
import { getActionType as getWebhookActionType } from './webhook';
import { getActionType as getServiceNowActionType } from './servicenow';
import { getActionType as getJiraActionType } from './jira';
+import { getActionType as getResilientActionType } from './resilient';
export function registerBuiltInActionTypes({
actionsConfigUtils: configurationUtilities,
@@ -34,4 +35,5 @@ export function registerBuiltInActionTypes({
actionTypeRegistry.register(getWebhookActionType({ logger, configurationUtilities }));
actionTypeRegistry.register(getServiceNowActionType({ logger, configurationUtilities }));
actionTypeRegistry.register(getJiraActionType({ configurationUtilities }));
+ actionTypeRegistry.register(getResilientActionType({ configurationUtilities }));
}
diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/api.test.ts
new file mode 100644
index 0000000000000..734f6be382629
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/api.test.ts
@@ -0,0 +1,517 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { api } from '../case/api';
+import { externalServiceMock, mapping, apiParams } from './mocks';
+import { ExternalService } from '../case/types';
+
+describe('api', () => {
+ let externalService: jest.Mocked;
+
+ beforeEach(() => {
+ externalService = externalServiceMock.create();
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('pushToService', () => {
+ describe('create incident', () => {
+ test('it creates an incident', async () => {
+ const params = { ...apiParams, externalId: null };
+ const res = await api.pushToService({ externalService, mapping, params });
+
+ expect(res).toEqual({
+ id: '1',
+ title: '1',
+ pushedDate: '2020-06-03T15:09:13.606Z',
+ url: 'https://resilient.elastic.co/#incidents/1',
+ comments: [
+ {
+ commentId: 'case-comment-1',
+ pushedDate: '2020-06-03T15:09:13.606Z',
+ },
+ {
+ commentId: 'case-comment-2',
+ pushedDate: '2020-06-03T15:09:13.606Z',
+ },
+ ],
+ });
+ });
+
+ test('it creates an incident without comments', async () => {
+ const params = { ...apiParams, externalId: null, comments: [] };
+ const res = await api.pushToService({ externalService, mapping, params });
+
+ expect(res).toEqual({
+ id: '1',
+ title: '1',
+ pushedDate: '2020-06-03T15:09:13.606Z',
+ url: 'https://resilient.elastic.co/#incidents/1',
+ });
+ });
+
+ test('it calls createIncident correctly', async () => {
+ const params = { ...apiParams, externalId: null };
+ await api.pushToService({ externalService, mapping, params });
+
+ expect(externalService.createIncident).toHaveBeenCalledWith({
+ incident: {
+ description:
+ 'Incident description (created at 2020-06-03T15:09:13.606Z by Elastic User)',
+ name: 'Incident title (created at 2020-06-03T15:09:13.606Z by Elastic User)',
+ },
+ });
+ expect(externalService.updateIncident).not.toHaveBeenCalled();
+ });
+
+ test('it calls createComment correctly', async () => {
+ const params = { ...apiParams, externalId: null };
+ await api.pushToService({ externalService, mapping, params });
+ expect(externalService.createComment).toHaveBeenCalledTimes(2);
+ expect(externalService.createComment).toHaveBeenNthCalledWith(1, {
+ incidentId: '1',
+ comment: {
+ commentId: 'case-comment-1',
+ comment: 'A comment (added at 2020-06-03T15:09:13.606Z by Elastic User)',
+ createdAt: '2020-06-03T15:09:13.606Z',
+ createdBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ updatedAt: '2020-06-03T15:09:13.606Z',
+ updatedBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ },
+ field: 'comments',
+ });
+
+ expect(externalService.createComment).toHaveBeenNthCalledWith(2, {
+ incidentId: '1',
+ comment: {
+ commentId: 'case-comment-2',
+ comment: 'Another comment (added at 2020-06-03T15:09:13.606Z by Elastic User)',
+ createdAt: '2020-06-03T15:09:13.606Z',
+ createdBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ updatedAt: '2020-06-03T15:09:13.606Z',
+ updatedBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ },
+ field: 'comments',
+ });
+ });
+ });
+
+ describe('update incident', () => {
+ test('it updates an incident', async () => {
+ const res = await api.pushToService({ externalService, mapping, params: apiParams });
+
+ expect(res).toEqual({
+ id: '1',
+ title: '1',
+ pushedDate: '2020-06-03T15:09:13.606Z',
+ url: 'https://resilient.elastic.co/#incidents/1',
+ comments: [
+ {
+ commentId: 'case-comment-1',
+ pushedDate: '2020-06-03T15:09:13.606Z',
+ },
+ {
+ commentId: 'case-comment-2',
+ pushedDate: '2020-06-03T15:09:13.606Z',
+ },
+ ],
+ });
+ });
+
+ test('it updates an incident without comments', async () => {
+ const params = { ...apiParams, comments: [] };
+ const res = await api.pushToService({ externalService, mapping, params });
+
+ expect(res).toEqual({
+ id: '1',
+ title: '1',
+ pushedDate: '2020-06-03T15:09:13.606Z',
+ url: 'https://resilient.elastic.co/#incidents/1',
+ });
+ });
+
+ test('it calls updateIncident correctly', async () => {
+ const params = { ...apiParams };
+ await api.pushToService({ externalService, mapping, params });
+
+ expect(externalService.updateIncident).toHaveBeenCalledWith({
+ incidentId: 'incident-3',
+ incident: {
+ description:
+ 'Incident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
+ name: 'Incident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
+ },
+ });
+ expect(externalService.createIncident).not.toHaveBeenCalled();
+ });
+
+ test('it calls createComment correctly', async () => {
+ const params = { ...apiParams };
+ await api.pushToService({ externalService, mapping, params });
+ expect(externalService.createComment).toHaveBeenCalledTimes(2);
+ expect(externalService.createComment).toHaveBeenNthCalledWith(1, {
+ incidentId: '1',
+ comment: {
+ commentId: 'case-comment-1',
+ comment: 'A comment (added at 2020-06-03T15:09:13.606Z by Elastic User)',
+ createdAt: '2020-06-03T15:09:13.606Z',
+ createdBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ updatedAt: '2020-06-03T15:09:13.606Z',
+ updatedBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ },
+ field: 'comments',
+ });
+
+ expect(externalService.createComment).toHaveBeenNthCalledWith(2, {
+ incidentId: '1',
+ comment: {
+ commentId: 'case-comment-2',
+ comment: 'Another comment (added at 2020-06-03T15:09:13.606Z by Elastic User)',
+ createdAt: '2020-06-03T15:09:13.606Z',
+ createdBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ updatedAt: '2020-06-03T15:09:13.606Z',
+ updatedBy: {
+ fullName: 'Elastic User',
+ username: 'elastic',
+ },
+ },
+ field: 'comments',
+ });
+ });
+ });
+
+ describe('mapping variations', () => {
+ test('overwrite & append', async () => {
+ mapping.set('title', {
+ target: 'name',
+ actionType: 'overwrite',
+ });
+
+ mapping.set('description', {
+ target: 'description',
+ actionType: 'append',
+ });
+
+ mapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+ });
+
+ mapping.set('name', {
+ target: 'title',
+ actionType: 'overwrite',
+ });
+
+ await api.pushToService({ externalService, mapping, params: apiParams });
+ expect(externalService.updateIncident).toHaveBeenCalledWith({
+ incidentId: 'incident-3',
+ incident: {
+ name: 'Incident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
+ description:
+ 'description from ibm resilient \r\nIncident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
+ },
+ });
+ });
+
+ test('nothing & append', async () => {
+ mapping.set('title', {
+ target: 'name',
+ actionType: 'nothing',
+ });
+
+ mapping.set('description', {
+ target: 'description',
+ actionType: 'append',
+ });
+
+ mapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+ });
+
+ mapping.set('name', {
+ target: 'title',
+ actionType: 'nothing',
+ });
+
+ await api.pushToService({ externalService, mapping, params: apiParams });
+ expect(externalService.updateIncident).toHaveBeenCalledWith({
+ incidentId: 'incident-3',
+ incident: {
+ description:
+ 'description from ibm resilient \r\nIncident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
+ },
+ });
+ });
+
+ test('append & append', async () => {
+ mapping.set('title', {
+ target: 'name',
+ actionType: 'append',
+ });
+
+ mapping.set('description', {
+ target: 'description',
+ actionType: 'append',
+ });
+
+ mapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+ });
+
+ mapping.set('name', {
+ target: 'title',
+ actionType: 'append',
+ });
+
+ await api.pushToService({ externalService, mapping, params: apiParams });
+ expect(externalService.updateIncident).toHaveBeenCalledWith({
+ incidentId: 'incident-3',
+ incident: {
+ name:
+ 'title from ibm resilient \r\nIncident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
+ description:
+ 'description from ibm resilient \r\nIncident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
+ },
+ });
+ });
+
+ test('nothing & nothing', async () => {
+ mapping.set('title', {
+ target: 'name',
+ actionType: 'nothing',
+ });
+
+ mapping.set('description', {
+ target: 'description',
+ actionType: 'nothing',
+ });
+
+ mapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+ });
+
+ mapping.set('name', {
+ target: 'title',
+ actionType: 'nothing',
+ });
+
+ await api.pushToService({ externalService, mapping, params: apiParams });
+ expect(externalService.updateIncident).toHaveBeenCalledWith({
+ incidentId: 'incident-3',
+ incident: {},
+ });
+ });
+
+ test('overwrite & nothing', async () => {
+ mapping.set('title', {
+ target: 'name',
+ actionType: 'overwrite',
+ });
+
+ mapping.set('description', {
+ target: 'description',
+ actionType: 'nothing',
+ });
+
+ mapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+ });
+
+ mapping.set('name', {
+ target: 'title',
+ actionType: 'overwrite',
+ });
+
+ await api.pushToService({ externalService, mapping, params: apiParams });
+ expect(externalService.updateIncident).toHaveBeenCalledWith({
+ incidentId: 'incident-3',
+ incident: {
+ name: 'Incident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
+ },
+ });
+ });
+
+ test('overwrite & overwrite', async () => {
+ mapping.set('title', {
+ target: 'name',
+ actionType: 'overwrite',
+ });
+
+ mapping.set('description', {
+ target: 'description',
+ actionType: 'overwrite',
+ });
+
+ mapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+ });
+
+ mapping.set('name', {
+ target: 'title',
+ actionType: 'overwrite',
+ });
+
+ await api.pushToService({ externalService, mapping, params: apiParams });
+ expect(externalService.updateIncident).toHaveBeenCalledWith({
+ incidentId: 'incident-3',
+ incident: {
+ name: 'Incident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
+ description:
+ 'Incident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
+ },
+ });
+ });
+
+ test('nothing & overwrite', async () => {
+ mapping.set('title', {
+ target: 'name',
+ actionType: 'nothing',
+ });
+
+ mapping.set('description', {
+ target: 'description',
+ actionType: 'overwrite',
+ });
+
+ mapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+ });
+
+ mapping.set('name', {
+ target: 'title',
+ actionType: 'nothing',
+ });
+
+ await api.pushToService({ externalService, mapping, params: apiParams });
+ expect(externalService.updateIncident).toHaveBeenCalledWith({
+ incidentId: 'incident-3',
+ incident: {
+ description:
+ 'Incident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
+ },
+ });
+ });
+
+ test('append & overwrite', async () => {
+ mapping.set('title', {
+ target: 'name',
+ actionType: 'append',
+ });
+
+ mapping.set('description', {
+ target: 'description',
+ actionType: 'overwrite',
+ });
+
+ mapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+ });
+
+ mapping.set('name', {
+ target: 'title',
+ actionType: 'append',
+ });
+
+ await api.pushToService({ externalService, mapping, params: apiParams });
+ expect(externalService.updateIncident).toHaveBeenCalledWith({
+ incidentId: 'incident-3',
+ incident: {
+ name:
+ 'title from ibm resilient \r\nIncident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
+ description:
+ 'Incident description (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
+ },
+ });
+ });
+
+ test('append & nothing', async () => {
+ mapping.set('title', {
+ target: 'name',
+ actionType: 'append',
+ });
+
+ mapping.set('description', {
+ target: 'description',
+ actionType: 'nothing',
+ });
+
+ mapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+ });
+
+ mapping.set('name', {
+ target: 'title',
+ actionType: 'append',
+ });
+
+ await api.pushToService({ externalService, mapping, params: apiParams });
+ expect(externalService.updateIncident).toHaveBeenCalledWith({
+ incidentId: 'incident-3',
+ incident: {
+ name:
+ 'title from ibm resilient \r\nIncident title (updated at 2020-06-03T15:09:13.606Z by Elastic User)',
+ },
+ });
+ });
+
+ test('comment nothing', async () => {
+ mapping.set('title', {
+ target: 'name',
+ actionType: 'overwrite',
+ });
+
+ mapping.set('description', {
+ target: 'description',
+ actionType: 'nothing',
+ });
+
+ mapping.set('comments', {
+ target: 'comments',
+ actionType: 'nothing',
+ });
+
+ mapping.set('name', {
+ target: 'title',
+ actionType: 'overwrite',
+ });
+
+ await api.pushToService({ externalService, mapping, params: apiParams });
+ expect(externalService.createComment).not.toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/index.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/api.ts
similarity index 87%
rename from x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/index.ts
rename to x-pack/plugins/actions/server/builtin_action_types/resilient/api.ts
index 41bc2aa258807..3db66e5884af4 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/index.ts
+++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/api.ts
@@ -3,3 +3,5 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
+
+export { api } from '../case/api';
diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/config.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/config.ts
new file mode 100644
index 0000000000000..4ce9417bfa9a1
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/config.ts
@@ -0,0 +1,14 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { ExternalServiceConfiguration } from '../case/types';
+import * as i18n from './translations';
+
+export const config: ExternalServiceConfiguration = {
+ id: '.resilient',
+ name: i18n.NAME,
+ minimumLicenseRequired: 'platinum',
+};
diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts
new file mode 100644
index 0000000000000..e98bc71559d3f
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { createConnector } from '../case/utils';
+
+import { api } from './api';
+import { config } from './config';
+import { validate } from './validators';
+import { createExternalService } from './service';
+import { ResilientSecretConfiguration, ResilientPublicConfiguration } from './schema';
+
+export const getActionType = createConnector({
+ api,
+ config,
+ validate,
+ createExternalService,
+ validationSchema: {
+ config: ResilientPublicConfiguration,
+ secrets: ResilientSecretConfiguration,
+ },
+});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/mocks.ts
new file mode 100644
index 0000000000000..bba9c58bf28c9
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/mocks.ts
@@ -0,0 +1,124 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ ExternalService,
+ PushToServiceApiParams,
+ ExecutorSubActionPushParams,
+ MapRecord,
+} from '../case/types';
+
+const createMock = (): jest.Mocked => {
+ const service = {
+ getIncident: jest.fn().mockImplementation(() =>
+ Promise.resolve({
+ id: '1',
+ name: 'title from ibm resilient',
+ description: 'description from ibm resilient',
+ discovered_date: 1589391874472,
+ create_date: 1591192608323,
+ inc_last_modified_date: 1591192650372,
+ })
+ ),
+ createIncident: jest.fn().mockImplementation(() =>
+ Promise.resolve({
+ id: '1',
+ title: '1',
+ pushedDate: '2020-06-03T15:09:13.606Z',
+ url: 'https://resilient.elastic.co/#incidents/1',
+ })
+ ),
+ updateIncident: jest.fn().mockImplementation(() =>
+ Promise.resolve({
+ id: '1',
+ title: '1',
+ pushedDate: '2020-06-03T15:09:13.606Z',
+ url: 'https://resilient.elastic.co/#incidents/1',
+ })
+ ),
+ createComment: jest.fn(),
+ };
+
+ service.createComment.mockImplementationOnce(() =>
+ Promise.resolve({
+ commentId: 'case-comment-1',
+ pushedDate: '2020-06-03T15:09:13.606Z',
+ externalCommentId: '1',
+ })
+ );
+
+ service.createComment.mockImplementationOnce(() =>
+ Promise.resolve({
+ commentId: 'case-comment-2',
+ pushedDate: '2020-06-03T15:09:13.606Z',
+ externalCommentId: '2',
+ })
+ );
+
+ return service;
+};
+
+const externalServiceMock = {
+ create: createMock,
+};
+
+const mapping: Map> = new Map();
+
+mapping.set('title', {
+ target: 'name',
+ actionType: 'overwrite',
+});
+
+mapping.set('description', {
+ target: 'description',
+ actionType: 'overwrite',
+});
+
+mapping.set('comments', {
+ target: 'comments',
+ actionType: 'append',
+});
+
+mapping.set('name', {
+ target: 'title',
+ actionType: 'overwrite',
+});
+
+const executorParams: ExecutorSubActionPushParams = {
+ savedObjectId: 'd4387ac5-0899-4dc2-bbfa-0dd605c934aa',
+ externalId: 'incident-3',
+ createdAt: '2020-06-03T15:09:13.606Z',
+ createdBy: { fullName: 'Elastic User', username: 'elastic' },
+ updatedAt: '2020-06-03T15:09:13.606Z',
+ updatedBy: { fullName: 'Elastic User', username: 'elastic' },
+ title: 'Incident title',
+ description: 'Incident description',
+ comments: [
+ {
+ commentId: 'case-comment-1',
+ comment: 'A comment',
+ createdAt: '2020-06-03T15:09:13.606Z',
+ createdBy: { fullName: 'Elastic User', username: 'elastic' },
+ updatedAt: '2020-06-03T15:09:13.606Z',
+ updatedBy: { fullName: 'Elastic User', username: 'elastic' },
+ },
+ {
+ commentId: 'case-comment-2',
+ comment: 'Another comment',
+ createdAt: '2020-06-03T15:09:13.606Z',
+ createdBy: { fullName: 'Elastic User', username: 'elastic' },
+ updatedAt: '2020-06-03T15:09:13.606Z',
+ updatedBy: { fullName: 'Elastic User', username: 'elastic' },
+ },
+ ],
+};
+
+const apiParams: PushToServiceApiParams = {
+ ...executorParams,
+ externalCase: { name: 'Incident title', description: 'Incident description' },
+};
+
+export { externalServiceMock, mapping, executorParams, apiParams };
diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts
new file mode 100644
index 0000000000000..c13de2b27e2b9
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { schema } from '@kbn/config-schema';
+import { ExternalIncidentServiceConfiguration } from '../case/schema';
+
+export const ResilientPublicConfiguration = {
+ orgId: schema.string(),
+ ...ExternalIncidentServiceConfiguration,
+};
+
+export const ResilientPublicConfigurationSchema = schema.object(ResilientPublicConfiguration);
+
+export const ResilientSecretConfiguration = {
+ apiKeyId: schema.string(),
+ apiKeySecret: schema.string(),
+};
+
+export const ResilientSecretConfigurationSchema = schema.object(ResilientSecretConfiguration);
diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts
new file mode 100644
index 0000000000000..573885698014e
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts
@@ -0,0 +1,422 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import axios from 'axios';
+
+import { createExternalService, getValueTextContent, formatUpdateRequest } from './service';
+import * as utils from '../lib/axios_utils';
+import { ExternalService } from '../case/types';
+
+jest.mock('axios');
+jest.mock('../lib/axios_utils', () => {
+ const originalUtils = jest.requireActual('../lib/axios_utils');
+ return {
+ ...originalUtils,
+ request: jest.fn(),
+ };
+});
+
+axios.create = jest.fn(() => axios);
+const requestMock = utils.request as jest.Mock;
+const now = Date.now;
+const TIMESTAMP = 1589391874472;
+
+// Incident update makes three calls to the API.
+// The function below mocks this calls.
+// a) Get the latest incident
+// b) Update the incident
+// c) Get the updated incident
+const mockIncidentUpdate = (withUpdateError = false) => {
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ id: '1',
+ name: 'title',
+ description: {
+ format: 'html',
+ content: 'description',
+ },
+ },
+ }));
+
+ if (withUpdateError) {
+ requestMock.mockImplementationOnce(() => {
+ throw new Error('An error has occurred');
+ });
+ } else {
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ success: true,
+ id: '1',
+ inc_last_modified_date: 1589391874472,
+ },
+ }));
+ }
+
+ requestMock.mockImplementationOnce(() => ({
+ data: {
+ id: '1',
+ name: 'title_updated',
+ description: {
+ format: 'html',
+ content: 'desc_updated',
+ },
+ inc_last_modified_date: 1589391874472,
+ },
+ }));
+};
+
+describe('IBM Resilient service', () => {
+ let service: ExternalService;
+
+ beforeAll(() => {
+ service = createExternalService({
+ config: { apiUrl: 'https://resilient.elastic.co', orgId: '201' },
+ secrets: { apiKeyId: 'keyId', apiKeySecret: 'secret' },
+ });
+ });
+
+ afterAll(() => {
+ Date.now = now;
+ });
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ Date.now = jest.fn().mockReturnValue(TIMESTAMP);
+ });
+
+ describe('getValueTextContent', () => {
+ test('transforms correctly', () => {
+ expect(getValueTextContent('name', 'title')).toEqual({
+ text: 'title',
+ });
+ });
+
+ test('transforms correctly the description', () => {
+ expect(getValueTextContent('description', 'desc')).toEqual({
+ textarea: {
+ format: 'html',
+ content: 'desc',
+ },
+ });
+ });
+ });
+
+ describe('formatUpdateRequest', () => {
+ test('transforms correctly', () => {
+ const oldIncident = { name: 'title', description: 'desc' };
+ const newIncident = { name: 'title_updated', description: 'desc_updated' };
+ expect(formatUpdateRequest({ oldIncident, newIncident })).toEqual({
+ changes: [
+ {
+ field: { name: 'name' },
+ old_value: { text: 'title' },
+ new_value: { text: 'title_updated' },
+ },
+ {
+ field: { name: 'description' },
+ old_value: {
+ textarea: {
+ format: 'html',
+ content: 'desc',
+ },
+ },
+ new_value: {
+ textarea: {
+ format: 'html',
+ content: 'desc_updated',
+ },
+ },
+ },
+ ],
+ });
+ });
+ });
+
+ describe('createExternalService', () => {
+ test('throws without url', () => {
+ expect(() =>
+ createExternalService({
+ config: { apiUrl: null, orgId: '201' },
+ secrets: { apiKeyId: 'token', apiKeySecret: 'secret' },
+ })
+ ).toThrow();
+ });
+
+ test('throws without orgId', () => {
+ expect(() =>
+ createExternalService({
+ config: { apiUrl: 'test.com', orgId: null },
+ secrets: { apiKeyId: 'token', apiKeySecret: 'secret' },
+ })
+ ).toThrow();
+ });
+
+ test('throws without username', () => {
+ expect(() =>
+ createExternalService({
+ config: { apiUrl: 'test.com', orgId: '201' },
+ secrets: { apiKeyId: '', apiKeySecret: 'secret' },
+ })
+ ).toThrow();
+ });
+
+ test('throws without password', () => {
+ expect(() =>
+ createExternalService({
+ config: { apiUrl: 'test.com', orgId: '201' },
+ secrets: { apiKeyId: '', apiKeySecret: undefined },
+ })
+ ).toThrow();
+ });
+ });
+
+ describe('getIncident', () => {
+ test('it returns the incident correctly', async () => {
+ requestMock.mockImplementation(() => ({
+ data: {
+ id: '1',
+ name: '1',
+ description: {
+ format: 'html',
+ content: 'description',
+ },
+ },
+ }));
+ const res = await service.getIncident('1');
+ expect(res).toEqual({ id: '1', name: '1', description: 'description' });
+ });
+
+ test('it should call request with correct arguments', async () => {
+ requestMock.mockImplementation(() => ({
+ data: { id: '1' },
+ }));
+
+ await service.getIncident('1');
+ expect(requestMock).toHaveBeenCalledWith({
+ axios,
+ url: 'https://resilient.elastic.co/rest/orgs/201/incidents/1',
+ params: {
+ text_content_output_format: 'objects_convert',
+ },
+ });
+ });
+
+ test('it should throw an error', async () => {
+ requestMock.mockImplementation(() => {
+ throw new Error('An error has occurred');
+ });
+ expect(service.getIncident('1')).rejects.toThrow(
+ 'Unable to get incident with id 1. Error: An error has occurred'
+ );
+ });
+ });
+
+ describe('createIncident', () => {
+ test('it creates the incident correctly', async () => {
+ requestMock.mockImplementation(() => ({
+ data: {
+ id: '1',
+ name: 'title',
+ description: 'description',
+ discovered_date: 1589391874472,
+ create_date: 1589391874472,
+ },
+ }));
+
+ const res = await service.createIncident({
+ incident: { name: 'title', description: 'desc' },
+ });
+
+ expect(res).toEqual({
+ title: '1',
+ id: '1',
+ pushedDate: '2020-05-13T17:44:34.472Z',
+ url: 'https://resilient.elastic.co/#incidents/1',
+ });
+ });
+
+ test('it should call request with correct arguments', async () => {
+ requestMock.mockImplementation(() => ({
+ data: {
+ id: '1',
+ name: 'title',
+ description: 'description',
+ discovered_date: 1589391874472,
+ create_date: 1589391874472,
+ },
+ }));
+
+ await service.createIncident({
+ incident: { name: 'title', description: 'desc' },
+ });
+
+ expect(requestMock).toHaveBeenCalledWith({
+ axios,
+ url: 'https://resilient.elastic.co/rest/orgs/201/incidents',
+ method: 'post',
+ data: {
+ name: 'title',
+ description: {
+ format: 'html',
+ content: 'desc',
+ },
+ discovered_date: TIMESTAMP,
+ },
+ });
+ });
+
+ test('it should throw an error', async () => {
+ requestMock.mockImplementation(() => {
+ throw new Error('An error has occurred');
+ });
+
+ expect(
+ service.createIncident({
+ incident: { name: 'title', description: 'desc' },
+ })
+ ).rejects.toThrow(
+ '[Action][IBM Resilient]: Unable to create incident. Error: An error has occurred'
+ );
+ });
+ });
+
+ describe('updateIncident', () => {
+ test('it updates the incident correctly', async () => {
+ mockIncidentUpdate();
+ const res = await service.updateIncident({
+ incidentId: '1',
+ incident: { name: 'title_updated', description: 'desc_updated' },
+ });
+
+ expect(res).toEqual({
+ title: '1',
+ id: '1',
+ pushedDate: '2020-05-13T17:44:34.472Z',
+ url: 'https://resilient.elastic.co/#incidents/1',
+ });
+ });
+
+ test('it should call request with correct arguments', async () => {
+ mockIncidentUpdate();
+
+ await service.updateIncident({
+ incidentId: '1',
+ incident: { name: 'title_updated', description: 'desc_updated' },
+ });
+
+ // Incident update makes three calls to the API.
+ // The second call to the API is the update call.
+ expect(requestMock.mock.calls[1][0]).toEqual({
+ axios,
+ method: 'patch',
+ url: 'https://resilient.elastic.co/rest/orgs/201/incidents/1',
+ data: {
+ changes: [
+ {
+ field: { name: 'name' },
+ old_value: { text: 'title' },
+ new_value: { text: 'title_updated' },
+ },
+ {
+ field: { name: 'description' },
+ old_value: {
+ textarea: {
+ content: 'description',
+ format: 'html',
+ },
+ },
+ new_value: {
+ textarea: {
+ content: 'desc_updated',
+ format: 'html',
+ },
+ },
+ },
+ ],
+ },
+ });
+ });
+
+ test('it should throw an error', async () => {
+ mockIncidentUpdate(true);
+
+ expect(
+ service.updateIncident({
+ incidentId: '1',
+ incident: { name: 'title', description: 'desc' },
+ })
+ ).rejects.toThrow(
+ '[Action][IBM Resilient]: Unable to update incident with id 1. Error: An error has occurred'
+ );
+ });
+ });
+
+ describe('createComment', () => {
+ test('it creates the comment correctly', async () => {
+ requestMock.mockImplementation(() => ({
+ data: {
+ id: '1',
+ create_date: 1589391874472,
+ },
+ }));
+
+ const res = await service.createComment({
+ incidentId: '1',
+ comment: { comment: 'comment', commentId: 'comment-1' },
+ field: 'comments',
+ });
+
+ expect(res).toEqual({
+ commentId: 'comment-1',
+ pushedDate: '2020-05-13T17:44:34.472Z',
+ externalCommentId: '1',
+ });
+ });
+
+ test('it should call request with correct arguments', async () => {
+ requestMock.mockImplementation(() => ({
+ data: {
+ id: '1',
+ create_date: 1589391874472,
+ },
+ }));
+
+ await service.createComment({
+ incidentId: '1',
+ comment: { comment: 'comment', commentId: 'comment-1' },
+ field: 'my_field',
+ });
+
+ expect(requestMock).toHaveBeenCalledWith({
+ axios,
+ method: 'post',
+ url: 'https://resilient.elastic.co/rest/orgs/201/incidents/1/comments',
+ data: {
+ text: {
+ content: 'comment',
+ format: 'text',
+ },
+ },
+ });
+ });
+
+ test('it should throw an error', async () => {
+ requestMock.mockImplementation(() => {
+ throw new Error('An error has occurred');
+ });
+
+ expect(
+ service.createComment({
+ incidentId: '1',
+ comment: { comment: 'comment', commentId: 'comment-1' },
+ field: 'comments',
+ })
+ ).rejects.toThrow(
+ '[Action][IBM Resilient]: Unable to create comment at incident with id 1. Error: An error has occurred'
+ );
+ });
+ });
+});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts
new file mode 100644
index 0000000000000..8d0526ca3b571
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts
@@ -0,0 +1,197 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import axios from 'axios';
+
+import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types';
+import {
+ ResilientPublicConfigurationType,
+ ResilientSecretConfigurationType,
+ CreateIncidentRequest,
+ UpdateIncidentRequest,
+ CreateCommentRequest,
+ UpdateFieldText,
+ UpdateFieldTextArea,
+} from './types';
+
+import * as i18n from './translations';
+import { getErrorMessage, request } from '../lib/axios_utils';
+
+const BASE_URL = `rest`;
+const INCIDENT_URL = `incidents`;
+const COMMENT_URL = `comments`;
+
+const VIEW_INCIDENT_URL = `#incidents`;
+
+export const getValueTextContent = (
+ field: string,
+ value: string
+): UpdateFieldText | UpdateFieldTextArea => {
+ if (field === 'description') {
+ return {
+ textarea: {
+ format: 'html',
+ content: value,
+ },
+ };
+ }
+
+ return {
+ text: value,
+ };
+};
+
+export const formatUpdateRequest = ({
+ oldIncident,
+ newIncident,
+}: ExternalServiceParams): UpdateIncidentRequest => {
+ return {
+ changes: Object.keys(newIncident).map((key) => ({
+ field: { name: key },
+ old_value: getValueTextContent(key, oldIncident[key]),
+ new_value: getValueTextContent(key, newIncident[key]),
+ })),
+ };
+};
+
+export const createExternalService = ({
+ config,
+ secrets,
+}: ExternalServiceCredentials): ExternalService => {
+ const { apiUrl: url, orgId } = config as ResilientPublicConfigurationType;
+ const { apiKeyId, apiKeySecret } = secrets as ResilientSecretConfigurationType;
+
+ if (!url || !orgId || !apiKeyId || !apiKeySecret) {
+ throw Error(`[Action]${i18n.NAME}: Wrong configuration.`);
+ }
+
+ const urlWithoutTrailingSlash = url.endsWith('/') ? url.slice(0, -1) : url;
+ const incidentUrl = `${urlWithoutTrailingSlash}/${BASE_URL}/orgs/${orgId}/${INCIDENT_URL}`;
+ const commentUrl = `${incidentUrl}/{inc_id}/${COMMENT_URL}`;
+ const axiosInstance = axios.create({
+ auth: { username: apiKeyId, password: apiKeySecret },
+ });
+
+ const getIncidentViewURL = (key: string) => {
+ return `${urlWithoutTrailingSlash}/${VIEW_INCIDENT_URL}/${key}`;
+ };
+
+ const getCommentsURL = (incidentId: string) => {
+ return commentUrl.replace('{inc_id}', incidentId);
+ };
+
+ const getIncident = async (id: string) => {
+ try {
+ const res = await request({
+ axios: axiosInstance,
+ url: `${incidentUrl}/${id}`,
+ params: {
+ text_content_output_format: 'objects_convert',
+ },
+ });
+
+ return { ...res.data, description: res.data.description?.content ?? '' };
+ } catch (error) {
+ throw new Error(
+ getErrorMessage(i18n.NAME, `Unable to get incident with id ${id}. Error: ${error.message}`)
+ );
+ }
+ };
+
+ const createIncident = async ({ incident }: ExternalServiceParams) => {
+ try {
+ const res = await request({
+ axios: axiosInstance,
+ url: `${incidentUrl}`,
+ method: 'post',
+ data: {
+ ...incident,
+ description: {
+ format: 'html',
+ content: incident.description ?? '',
+ },
+ discovered_date: Date.now(),
+ },
+ });
+
+ return {
+ title: `${res.data.id}`,
+ id: `${res.data.id}`,
+ pushedDate: new Date(res.data.create_date).toISOString(),
+ url: getIncidentViewURL(res.data.id),
+ };
+ } catch (error) {
+ throw new Error(
+ getErrorMessage(i18n.NAME, `Unable to create incident. Error: ${error.message}`)
+ );
+ }
+ };
+
+ const updateIncident = async ({ incidentId, incident }: ExternalServiceParams) => {
+ try {
+ const latestIncident = await getIncident(incidentId);
+
+ const data = formatUpdateRequest({ oldIncident: latestIncident, newIncident: incident });
+ const res = await request({
+ axios: axiosInstance,
+ method: 'patch',
+ url: `${incidentUrl}/${incidentId}`,
+ data,
+ });
+
+ if (!res.data.success) {
+ throw new Error(res.data.message);
+ }
+
+ const updatedIncident = await getIncident(incidentId);
+
+ return {
+ title: `${updatedIncident.id}`,
+ id: `${updatedIncident.id}`,
+ pushedDate: new Date(updatedIncident.inc_last_modified_date).toISOString(),
+ url: getIncidentViewURL(updatedIncident.id),
+ };
+ } catch (error) {
+ throw new Error(
+ getErrorMessage(
+ i18n.NAME,
+ `Unable to update incident with id ${incidentId}. Error: ${error.message}`
+ )
+ );
+ }
+ };
+
+ const createComment = async ({ incidentId, comment, field }: ExternalServiceParams) => {
+ try {
+ const res = await request({
+ axios: axiosInstance,
+ method: 'post',
+ url: getCommentsURL(incidentId),
+ data: { text: { format: 'text', content: comment.comment } },
+ });
+
+ return {
+ commentId: comment.commentId,
+ externalCommentId: res.data.id,
+ pushedDate: new Date(res.data.create_date).toISOString(),
+ };
+ } catch (error) {
+ throw new Error(
+ getErrorMessage(
+ i18n.NAME,
+ `Unable to create comment at incident with id ${incidentId}. Error: ${error.message}`
+ )
+ );
+ }
+ };
+
+ return {
+ getIncident,
+ createIncident,
+ updateIncident,
+ createComment,
+ };
+};
diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/translations.ts
new file mode 100644
index 0000000000000..d952838d5a2b3
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/translations.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const NAME = i18n.translate('xpack.actions.builtin.case.resilientTitle', {
+ defaultMessage: 'IBM Resilient',
+});
diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/types.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/types.ts
new file mode 100644
index 0000000000000..6869e2ff3a105
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/types.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { TypeOf } from '@kbn/config-schema';
+import { ResilientPublicConfigurationSchema, ResilientSecretConfigurationSchema } from './schema';
+
+export type ResilientPublicConfigurationType = TypeOf;
+export type ResilientSecretConfigurationType = TypeOf;
+
+interface CreateIncidentBasicRequestArgs {
+ name: string;
+ description: string;
+ discovered_date: number;
+}
+
+interface Comment {
+ text: { format: string; content: string };
+}
+
+interface CreateIncidentRequestArgs extends CreateIncidentBasicRequestArgs {
+ comments?: Comment[];
+}
+
+export interface UpdateFieldText {
+ text: string;
+}
+
+export interface UpdateFieldTextArea {
+ textarea: { format: 'html' | 'text'; content: string };
+}
+
+interface UpdateField {
+ field: { name: string };
+ old_value: UpdateFieldText | UpdateFieldTextArea;
+ new_value: UpdateFieldText | UpdateFieldTextArea;
+}
+
+export type CreateIncidentRequest = CreateIncidentRequestArgs;
+export type CreateCommentRequest = Comment;
+
+export interface UpdateIncidentRequest {
+ changes: UpdateField[];
+}
diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/validators.ts
new file mode 100644
index 0000000000000..7226071392bc6
--- /dev/null
+++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/validators.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { validateCommonConfig, validateCommonSecrets } from '../case/validators';
+import { ExternalServiceValidation } from '../case/types';
+
+export const validate: ExternalServiceValidation = {
+ config: validateCommonConfig,
+ secrets: validateCommonSecrets,
+};
diff --git a/x-pack/plugins/apm/common/anomaly_detection.ts b/x-pack/plugins/apm/common/anomaly_detection.ts
new file mode 100644
index 0000000000000..1fd927d82f186
--- /dev/null
+++ b/x-pack/plugins/apm/common/anomaly_detection.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export interface ServiceAnomalyStats {
+ transactionType?: string;
+ anomalyScore?: number;
+ actualValue?: number;
+ jobId?: string;
+}
diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts
index 43f3585d0ebb2..b50db270ef544 100644
--- a/x-pack/plugins/apm/common/service_map.ts
+++ b/x-pack/plugins/apm/common/service_map.ts
@@ -15,11 +15,13 @@ import {
SPAN_SUBTYPE,
SPAN_TYPE,
} from './elasticsearch_fieldnames';
+import { ServiceAnomalyStats } from './anomaly_detection';
export interface ServiceConnectionNode extends cytoscape.NodeDataDefinition {
[SERVICE_NAME]: string;
[SERVICE_ENVIRONMENT]: string | null;
[AGENT_NAME]: string;
+ serviceAnomalyStats?: ServiceAnomalyStats;
}
export interface ExternalConnectionNode extends cytoscape.NodeDataDefinition {
[SPAN_DESTINATION_SERVICE_RESOURCE]: string;
@@ -37,8 +39,10 @@ export interface Connection {
export interface ServiceNodeMetrics {
avgMemoryUsage: number | null;
avgCpuUsage: number | null;
- avgTransactionDuration: number | null;
- avgRequestsPerMinute: number | null;
+ transactionStats: {
+ avgTransactionDuration: number | null;
+ avgRequestsPerMinute: number | null;
+ };
avgErrorsPerMinute: number | null;
}
diff --git a/x-pack/plugins/apm/common/utils/range_filter.ts b/x-pack/plugins/apm/common/utils/range_filter.ts
index 08062cbf76bc6..9ffec18d95fb0 100644
--- a/x-pack/plugins/apm/common/utils/range_filter.ts
+++ b/x-pack/plugins/apm/common/utils/range_filter.ts
@@ -4,13 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export function rangeFilter(
- start: number,
- end: number,
- timestampField = '@timestamp'
-) {
+export function rangeFilter(start: number, end: number) {
return {
- [timestampField]: {
+ '@timestamp': {
gte: start,
lte: end,
format: 'epoch_millis',
diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json
index 56a9e226b6528..ee89abf59ee23 100644
--- a/x-pack/plugins/apm/kibana.json
+++ b/x-pack/plugins/apm/kibana.json
@@ -28,5 +28,10 @@
],
"extraPublicDirs": [
"public/style/variables"
+ ],
+ "requiredBundles": [
+ "kibanaReact",
+ "kibanaUtils",
+ "observability"
]
}
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx
index a09482d663f65..a173f4068db6a 100644
--- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx
+++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/List.test.tsx
@@ -11,6 +11,12 @@ import { ErrorGroupList } from '../index';
import props from './props.json';
import { MockUrlParamsContextProvider } from '../../../../../context/UrlParamsContext/MockUrlParamsContextProvider';
+jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => {
+ return {
+ htmlIdGenerator: () => () => `generated-id`,
+ };
+});
+
describe('ErrorGroupOverview -> List', () => {
beforeAll(() => {
mockMoment();
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap
index 6a20e3c103709..a86f7fdf41f4f 100644
--- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap
+++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.tsx.snap
@@ -133,6 +133,8 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = `
List should render with data 1`] = `
List should render with data 1`] = `
-
List should render with data 1`] = `
size="m"
/>
-
-
-
- 1
-
-
-
+
+
+ 1
+
+
+
+
+
List should render with data 1`] = `
size="m"
/>
-
+
diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx
new file mode 100644
index 0000000000000..410ba8b5027fb
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx
@@ -0,0 +1,158 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+import React from 'react';
+import styled from 'styled-components';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiTitle,
+ EuiIconTip,
+ EuiHealth,
+} from '@elastic/eui';
+import { useTheme } from '../../../../hooks/useTheme';
+import { fontSize, px } from '../../../../style/variables';
+import { asInteger, asDuration } from '../../../../utils/formatters';
+import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink';
+import { getSeverityColor, popoverWidth } from '../cytoscapeOptions';
+import { getSeverity } from '../../../../../common/ml_job_constants';
+import { TRANSACTION_REQUEST } from '../../../../../common/transaction_types';
+import { ServiceAnomalyStats } from '../../../../../common/anomaly_detection';
+
+const HealthStatusTitle = styled(EuiTitle)`
+ display: inline;
+ text-transform: uppercase;
+`;
+
+const VerticallyCentered = styled.div`
+ display: flex;
+ align-items: center;
+`;
+
+const SubduedText = styled.span`
+ color: ${({ theme }) => theme.eui.euiTextSubduedColor};
+`;
+
+const EnableText = styled.section`
+ color: ${({ theme }) => theme.eui.euiTextSubduedColor};
+ line-height: 1.4;
+ font-size: ${fontSize};
+ width: ${px(popoverWidth)};
+`;
+
+export const ContentLine = styled.section`
+ line-height: 2;
+`;
+
+interface Props {
+ serviceName: string;
+ serviceAnomalyStats: ServiceAnomalyStats | undefined;
+}
+export function AnomalyDetection({ serviceName, serviceAnomalyStats }: Props) {
+ const theme = useTheme();
+
+ const anomalyScore = serviceAnomalyStats?.anomalyScore;
+ const anomalySeverity = getSeverity(anomalyScore);
+ const actualValue = serviceAnomalyStats?.actualValue;
+ const mlJobId = serviceAnomalyStats?.jobId;
+ const transactionType =
+ serviceAnomalyStats?.transactionType ?? TRANSACTION_REQUEST;
+ const hasAnomalyDetectionScore = anomalyScore !== undefined;
+
+ return (
+ <>
+
+
+ {ANOMALY_DETECTION_TITLE}
+
+
+
+ {!mlJobId && {ANOMALY_DETECTION_DISABLED_TEXT} }
+
+ {hasAnomalyDetectionScore && (
+
+
+
+
+
+ {ANOMALY_DETECTION_SCORE_METRIC}
+
+
+
+
+ {getDisplayedAnomalyScore(anomalyScore as number)}
+ {actualValue && (
+ ({asDuration(actualValue)})
+ )}
+
+
+
+
+ )}
+ {mlJobId && !hasAnomalyDetectionScore && (
+ {ANOMALY_DETECTION_NO_DATA_TEXT}
+ )}
+ {mlJobId && (
+
+
+ {ANOMALY_DETECTION_LINK}
+
+
+ )}
+ >
+ );
+}
+
+function getDisplayedAnomalyScore(score: number) {
+ if (score > 0 && score < 1) {
+ return '< 1';
+ }
+ return asInteger(score);
+}
+
+const ANOMALY_DETECTION_TITLE = i18n.translate(
+ 'xpack.apm.serviceMap.anomalyDetectionPopoverTitle',
+ { defaultMessage: 'Anomaly Detection' }
+);
+
+const ANOMALY_DETECTION_TOOLTIP = i18n.translate(
+ 'xpack.apm.serviceMap.anomalyDetectionPopoverTooltip',
+ {
+ defaultMessage:
+ 'Service health indicators are powered by the anomaly detection feature in machine learning',
+ }
+);
+
+const ANOMALY_DETECTION_SCORE_METRIC = i18n.translate(
+ 'xpack.apm.serviceMap.anomalyDetectionPopoverScoreMetric',
+ { defaultMessage: 'Score (max.)' }
+);
+
+const ANOMALY_DETECTION_LINK = i18n.translate(
+ 'xpack.apm.serviceMap.anomalyDetectionPopoverLink',
+ { defaultMessage: 'View anomalies' }
+);
+
+const ANOMALY_DETECTION_DISABLED_TEXT = i18n.translate(
+ 'xpack.apm.serviceMap.anomalyDetectionPopoverDisabled',
+ {
+ defaultMessage:
+ 'Display service health indicators by enabling anomaly detection in APM settings.',
+ }
+);
+
+const ANOMALY_DETECTION_NO_DATA_TEXT = i18n.translate(
+ 'xpack.apm.serviceMap.anomalyDetectionPopoverNoData',
+ {
+ defaultMessage: `We couldn't find an anomaly score within the selected time range. See details in the anomaly explorer.`,
+ }
+);
diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx
index 78779bdcc2052..c696a93773ceb 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Contents.tsx
@@ -15,7 +15,7 @@ import React, { MouseEvent } from 'react';
import { Buttons } from './Buttons';
import { Info } from './Info';
import { ServiceMetricFetcher } from './ServiceMetricFetcher';
-import { popoverMinWidth } from '../cytoscapeOptions';
+import { popoverWidth } from '../cytoscapeOptions';
interface ContentsProps {
isService: boolean;
@@ -60,7 +60,7 @@ export function Contents({
@@ -68,16 +68,12 @@ export function Contents({
- {/* //TODO [APM ML] add service health stats here:
- isService && (
-
-
-
-
- )*/}
{isService ? (
-
+
) : (
)}
diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx
index 2edd36f0d1380..ccf147ed1d90d 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/Popover.stories.tsx
@@ -12,40 +12,33 @@ storiesOf('app/ServiceMap/Popover/ServiceMetricList', module)
.add('example', () => (
- ))
- .add('loading', () => (
-
))
.add('some null values', () => (
))
.add('all null values', () => (
));
diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx
index 718e43984d7f3..957678877a134 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricFetcher.tsx
@@ -5,23 +5,38 @@
*/
import React from 'react';
+import {
+ EuiLoadingSpinner,
+ EuiFlexGroup,
+ EuiHorizontalRule,
+ EuiText,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+import { isNumber } from 'lodash';
import { ServiceNodeMetrics } from '../../../../../common/service_map';
-import { useFetcher } from '../../../../hooks/useFetcher';
+import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher';
import { useUrlParams } from '../../../../hooks/useUrlParams';
import { ServiceMetricList } from './ServiceMetricList';
+import { AnomalyDetection } from './AnomalyDetection';
+import { ServiceAnomalyStats } from '../../../../../common/anomaly_detection';
interface ServiceMetricFetcherProps {
serviceName: string;
+ serviceAnomalyStats: ServiceAnomalyStats | undefined;
}
export function ServiceMetricFetcher({
serviceName,
+ serviceAnomalyStats,
}: ServiceMetricFetcherProps) {
const {
urlParams: { start, end, environment },
} = useUrlParams();
- const { data = {} as ServiceNodeMetrics, status } = useFetcher(
+ const {
+ data = { transactionStats: {} } as ServiceNodeMetrics,
+ status,
+ } = useFetcher(
(callApmApi) => {
if (serviceName && start && end) {
return callApmApi({
@@ -35,7 +50,62 @@ export function ServiceMetricFetcher({
preservePreviousData: false,
}
);
- const isLoading = status === 'loading';
- return ;
+ const isLoading =
+ status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING;
+
+ if (isLoading) {
+ return ;
+ }
+
+ const {
+ avgCpuUsage,
+ avgErrorsPerMinute,
+ avgMemoryUsage,
+ transactionStats: { avgRequestsPerMinute, avgTransactionDuration },
+ } = data;
+
+ const hasServiceData = [
+ avgCpuUsage,
+ avgErrorsPerMinute,
+ avgMemoryUsage,
+ avgRequestsPerMinute,
+ avgTransactionDuration,
+ ].some((stat) => isNumber(stat));
+
+ if (environment && !hasServiceData) {
+ return (
+
+ {i18n.translate('xpack.apm.serviceMap.popoverMetrics.noDataText', {
+ defaultMessage: `No data for selected environment. Try switching to another environment.`,
+ })}
+
+ );
+ }
+ return (
+ <>
+ {serviceAnomalyStats && (
+ <>
+
+
+ >
+ )}
+
+ >
+ );
+}
+
+function LoadingSpinner() {
+ return (
+
+
+
+ );
}
diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx
index d66be9c61e42d..f82f434e7ded1 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx
+++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/ServiceMetricList.tsx
@@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiFlexGroup, EuiLoadingSpinner } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { isNumber } from 'lodash';
import React from 'react';
@@ -12,18 +11,6 @@ import styled from 'styled-components';
import { ServiceNodeMetrics } from '../../../../../common/service_map';
import { asDuration, asPercent, tpmUnit } from '../../../../utils/formatters';
-function LoadingSpinner() {
- return (
-
-
-
- );
-}
-
export const ItemRow = styled('tr')`
line-height: 2;
`;
@@ -37,17 +24,13 @@ export const ItemDescription = styled('td')`
text-align: right;
`;
-interface ServiceMetricListProps extends ServiceNodeMetrics {
- isLoading: boolean;
-}
+type ServiceMetricListProps = ServiceNodeMetrics;
export function ServiceMetricList({
- avgTransactionDuration,
- avgRequestsPerMinute,
avgErrorsPerMinute,
avgCpuUsage,
avgMemoryUsage,
- isLoading,
+ transactionStats,
}: ServiceMetricListProps) {
const listItems = [
{
@@ -57,8 +40,8 @@ export function ServiceMetricList({
defaultMessage: 'Trans. duration (avg.)',
}
),
- description: isNumber(avgTransactionDuration)
- ? asDuration(avgTransactionDuration)
+ description: isNumber(transactionStats.avgTransactionDuration)
+ ? asDuration(transactionStats.avgTransactionDuration)
: null,
},
{
@@ -68,8 +51,10 @@ export function ServiceMetricList({
defaultMessage: 'Req. per minute (avg.)',
}
),
- description: isNumber(avgRequestsPerMinute)
- ? `${avgRequestsPerMinute.toFixed(2)} ${tpmUnit('request')}`
+ description: isNumber(transactionStats.avgRequestsPerMinute)
+ ? `${transactionStats.avgRequestsPerMinute.toFixed(2)} ${tpmUnit(
+ 'request'
+ )}`
: null,
},
{
@@ -100,9 +85,7 @@ export function ServiceMetricList({
},
];
- return isLoading ? (
-
- ) : (
+ return (
{listItems.map(
diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts
index 5a2a3d2a2644e..dfcfbee1806a4 100644
--- a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts
+++ b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts
@@ -10,10 +10,11 @@ import {
SPAN_DESTINATION_SERVICE_RESOURCE,
} from '../../../../common/elasticsearch_fieldnames';
import { EuiTheme } from '../../../../../observability/public';
-import { severity } from '../../../../common/ml_job_constants';
+import { severity, getSeverity } from '../../../../common/ml_job_constants';
import { defaultIcon, iconForNode } from './icons';
+import { ServiceAnomalyStats } from '../../../../common/anomaly_detection';
-export const popoverMinWidth = 280;
+export const popoverWidth = 280;
export function getSeverityColor(theme: EuiTheme, nodeSeverity?: string) {
switch (nodeSeverity) {
@@ -29,12 +30,19 @@ export function getSeverityColor(theme: EuiTheme, nodeSeverity?: string) {
}
}
+function getNodeSeverity(el: cytoscape.NodeSingular) {
+ const serviceAnomalyStats: ServiceAnomalyStats | undefined = el.data(
+ 'serviceAnomalyStats'
+ );
+ return getSeverity(serviceAnomalyStats?.anomalyScore);
+}
+
function getBorderColorFn(
theme: EuiTheme
): cytoscape.Css.MapperFunction {
return (el: cytoscape.NodeSingular) => {
- const hasAnomalyDetectionJob = el.data('ml_job_id') !== undefined;
- const nodeSeverity = el.data('anomaly_severity');
+ const hasAnomalyDetectionJob = el.data('serviceAnomalyStats') !== undefined;
+ const nodeSeverity = getNodeSeverity(el);
if (hasAnomalyDetectionJob) {
return (
getSeverityColor(theme, nodeSeverity) || theme.eui.euiColorMediumShade
@@ -51,7 +59,7 @@ const getBorderStyle: cytoscape.Css.MapperFunction<
cytoscape.NodeSingular,
cytoscape.Css.LineStyle
> = (el: cytoscape.NodeSingular) => {
- const nodeSeverity = el.data('anomaly_severity');
+ const nodeSeverity = getNodeSeverity(el);
if (nodeSeverity === severity.critical) {
return 'double';
} else {
@@ -60,7 +68,7 @@ const getBorderStyle: cytoscape.Css.MapperFunction<
};
function getBorderWidth(el: cytoscape.NodeSingular) {
- const nodeSeverity = el.data('anomaly_severity');
+ const nodeSeverity = getNodeSeverity(el);
if (nodeSeverity === severity.minor || nodeSeverity === severity.major) {
return 4;
diff --git a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx
index 1e6015a9589b0..2f41b9fedd1d1 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx
@@ -25,7 +25,7 @@ export function AgentConfigurations() {
(callApmApi) =>
callApmApi({ pathname: '/api/apm/settings/agent-configuration' }),
[],
- { preservePreviousData: false }
+ { preservePreviousData: false, showToastOnError: false }
);
useTrackPageview({ app: 'apm', path: 'agent_configuration' });
diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx
index 81655bc46c336..6f985d06dba9d 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx
@@ -10,9 +10,15 @@ import { i18n } from '@kbn/i18n';
import { EuiPanel } from '@elastic/eui';
import { JobsList } from './jobs_list';
import { AddEnvironments } from './add_environments';
-import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher';
+import { useFetcher } from '../../../../hooks/useFetcher';
import { LicensePrompt } from '../../../shared/LicensePrompt';
import { useLicense } from '../../../../hooks/useLicense';
+import { APIReturnType } from '../../../../services/rest/createCallApmApi';
+
+const DEFAULT_VALUE: APIReturnType<'/api/apm/settings/anomaly-detection'> = {
+ jobs: [],
+ hasLegacyJobs: false,
+};
export const AnomalyDetection = () => {
const license = useLicense();
@@ -20,17 +26,13 @@ export const AnomalyDetection = () => {
const [viewAddEnvironments, setViewAddEnvironments] = useState(false);
- const { refetch, data = [], status } = useFetcher(
+ const { refetch, data = DEFAULT_VALUE, status } = useFetcher(
(callApmApi) =>
callApmApi({ pathname: `/api/apm/settings/anomaly-detection` }),
[],
- { preservePreviousData: false }
+ { preservePreviousData: false, showToastOnError: false }
);
- const isLoading =
- status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING;
- const hasFetchFailure = status === FETCH_STATUS.FAILURE;
-
if (!hasValidLicense) {
return (
@@ -60,13 +62,13 @@ export const AnomalyDetection = () => {
{i18n.translate('xpack.apm.settings.anomalyDetection.descriptionText', {
defaultMessage:
- 'The Machine Learning anomaly detection integration enables application health status indicators in the Service map by identifying transaction duration anomalies.',
+ 'The Machine Learning anomaly detection integration enables application health status indicators for each configured environment in the Service map by identifying transaction duration anomalies.',
})}
{viewAddEnvironments ? (
environment)}
+ currentEnvironments={data.jobs.map(({ environment }) => environment)}
onCreateJobSuccess={() => {
refetch();
setViewAddEnvironments(false);
@@ -77,9 +79,9 @@ export const AnomalyDetection = () => {
/>
) : (
{
setViewAddEnvironments(true);
}}
diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx
index 30b4805011f03..83d19aa27ac11 100644
--- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx
+++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx
@@ -16,12 +16,14 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
+import { FETCH_STATUS } from '../../../../hooks/useFetcher';
import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable';
import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt';
import { AnomalyDetectionJobByEnv } from '../../../../../typings/anomaly_detection';
import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink';
import { MLLink } from '../../../shared/Links/MachineLearningLinks/MLLink';
import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values';
+import { LegacyJobsCallout } from './legacy_jobs_callout';
const columns: Array> = [
{
@@ -60,17 +62,22 @@ const columns: Array> = [
];
interface Props {
- isLoading: boolean;
- hasFetchFailure: boolean;
+ status: FETCH_STATUS;
onAddEnvironments: () => void;
anomalyDetectionJobsByEnv: AnomalyDetectionJobByEnv[];
+ hasLegacyJobs: boolean;
}
export const JobsList = ({
- isLoading,
- hasFetchFailure,
+ status,
onAddEnvironments,
anomalyDetectionJobsByEnv,
+ hasLegacyJobs,
}: Props) => {
+ const isLoading =
+ status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING;
+
+ const hasFetchFailure = status === FETCH_STATUS.FAILURE;
+
return (
@@ -91,7 +98,7 @@ export const JobsList = ({
{i18n.translate(
'xpack.apm.settings.anomalyDetection.jobList.addEnvironments',
{
- defaultMessage: 'Add environments',
+ defaultMessage: 'Create ML Job',
}
)}
@@ -101,7 +108,7 @@ export const JobsList = ({
@@ -131,6 +138,8 @@ export const JobsList = ({
items={isLoading || hasFetchFailure ? [] : anomalyDetectionJobsByEnv}
/>
+
+ {hasLegacyJobs && }
);
};
diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/legacy_jobs_callout.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/legacy_jobs_callout.tsx
new file mode 100644
index 0000000000000..54053097ab02e
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/legacy_jobs_callout.tsx
@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { EuiCallOut, EuiButton } from '@elastic/eui';
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { useApmPluginContext } from '../../../../hooks/useApmPluginContext';
+
+export function LegacyJobsCallout() {
+ const { core } = useApmPluginContext();
+ return (
+
+
+ {i18n.translate(
+ 'xpack.apm.settings.anomaly_detection.legacy_jobs.body',
+ {
+ defaultMessage:
+ 'We have discovered legacy Machine Learning jobs from our previous integration which are no longer being used in the APM app',
+ }
+ )}
+
+
+ {i18n.translate(
+ 'xpack.apm.settings.anomaly_detection.legacy_jobs.button',
+ { defaultMessage: 'Review jobs' }
+ )}
+
+
+ );
+}
diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx
index b4cf3a65fea35..c832d3ded6175 100644
--- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx
+++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.test.tsx
@@ -18,8 +18,24 @@ describe('MLJobLink', () => {
{ search: '?rangeFrom=now/w&rangeTo=now-4h' } as Location
);
- expect(href).toEqual(
- `/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now-4h))`
+ expect(href).toMatchInlineSnapshot(
+ `"/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now-4h))"`
+ );
+ });
+ it('should produce the correct URL with jobId, serviceName, and transactionType', async () => {
+ const href = await getRenderedHref(
+ () => (
+
+ ),
+ { search: '?rangeFrom=now/w&rangeTo=now-4h' } as Location
+ );
+
+ expect(href).toMatchInlineSnapshot(
+ `"/basepath/app/ml#/timeseriesexplorer?_g=(ml:(jobIds:!(myservicename-mytransactiontype-high_mean_response_time)),refreshInterval:(pause:true,value:'0'),time:(from:now%2Fw,to:now-4h))&_a=(mlTimeSeriesExplorer:(entities:(service.name:opbeans-test,transaction.type:request)))"`
);
});
});
diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx
index 1e1f9ea5f23b7..f3c5b49287293 100644
--- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx
+++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLJobLink.tsx
@@ -5,24 +5,35 @@
*/
import React from 'react';
-import { MLLink } from './MLLink';
+import { EuiLink } from '@elastic/eui';
+import { useTimeSeriesExplorerHref } from './useTimeSeriesExplorerHref';
interface Props {
jobId: string;
external?: boolean;
+ serviceName?: string;
+ transactionType?: string;
}
-export const MLJobLink: React.FC = (props) => {
- const query = {
- ml: { jobIds: [props.jobId] },
- };
+export const MLJobLink: React.FC = ({
+ jobId,
+ serviceName,
+ transactionType,
+ external,
+ children,
+}) => {
+ const href = useTimeSeriesExplorerHref({
+ jobId,
+ serviceName,
+ transactionType,
+ });
return (
-
);
};
diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.ts
new file mode 100644
index 0000000000000..625b9205b6ce0
--- /dev/null
+++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/useTimeSeriesExplorerHref.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import url from 'url';
+import querystring from 'querystring';
+import rison from 'rison-node';
+import { useLocation } from '../../../../hooks/useLocation';
+import { useApmPluginContext } from '../../../../hooks/useApmPluginContext';
+import { getTimepickerRisonData } from '../rison_helpers';
+
+export function useTimeSeriesExplorerHref({
+ jobId,
+ serviceName,
+ transactionType,
+}: {
+ jobId: string;
+ serviceName?: string;
+ transactionType?: string;
+}) {
+ const { core } = useApmPluginContext();
+ const location = useLocation();
+
+ const search = querystring.stringify(
+ {
+ _g: rison.encode({
+ ml: { jobIds: [jobId] },
+ ...getTimepickerRisonData(location.search),
+ }),
+ ...(serviceName && transactionType
+ ? {
+ _a: rison.encode({
+ mlTimeSeriesExplorer: {
+ entities: {
+ 'service.name': serviceName,
+ 'transaction.type': transactionType,
+ },
+ },
+ }),
+ }
+ : null),
+ },
+ undefined,
+ undefined,
+ {
+ encodeURIComponent(str: string) {
+ return str;
+ },
+ }
+ );
+
+ return url.format({
+ pathname: core.http.basePath.prepend('/app/ml'),
+ hash: url.format({ pathname: '/timeseriesexplorer', search }),
+ });
+}
diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts
index f31ad83666a17..6e3a29d9f3dbc 100644
--- a/x-pack/plugins/apm/public/plugin.ts
+++ b/x-pack/plugins/apm/public/plugin.ts
@@ -6,7 +6,6 @@
import { i18n } from '@kbn/i18n';
import { lazy } from 'react';
-import { euiThemeVars as theme } from '@kbn/ui-shared-deps/theme';
import { ConfigSchema } from '.';
import { ObservabilityPluginSetup } from '../../observability/public';
import {
@@ -83,7 +82,7 @@ export class ApmPlugin implements Plugin {
plugins.observability.dashboard.register({
appName: 'apm',
fetchData: async (params) => {
- return fetchLandingPageData(params, { theme });
+ return fetchLandingPageData(params);
},
hasData,
});
diff --git a/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts b/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts
index a14d827eeaec5..fd407a8bf72ad 100644
--- a/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts
+++ b/x-pack/plugins/apm/public/services/rest/observability.dashboard.test.ts
@@ -6,7 +6,6 @@
import { fetchLandingPageData, hasData } from './observability_dashboard';
import * as createCallApmApi from './createCallApmApi';
-import { euiThemeVars as theme } from '@kbn/ui-shared-deps/theme';
describe('Observability dashboard data', () => {
const callApmApiMock = jest.spyOn(createCallApmApi, 'callApmApi');
@@ -38,39 +37,31 @@ describe('Observability dashboard data', () => {
],
})
);
- const response = await fetchLandingPageData(
- {
- startTime: '1',
- endTime: '2',
- bucketSize: '3',
- },
- { theme }
- );
+ const response = await fetchLandingPageData({
+ startTime: '1',
+ endTime: '2',
+ bucketSize: '3',
+ });
expect(response).toEqual({
title: 'APM',
appLink: '/app/apm',
stats: {
services: {
type: 'number',
- label: 'Services',
value: 10,
},
transactions: {
type: 'number',
- label: 'Transactions',
value: 2,
- color: '#6092c0',
},
},
series: {
transactions: {
- label: 'Transactions',
coordinates: [
{ x: 1, y: 1 },
{ x: 2, y: 2 },
{ x: 3, y: 3 },
],
- color: '#6092c0',
},
},
});
@@ -82,35 +73,27 @@ describe('Observability dashboard data', () => {
transactionCoordinates: [],
})
);
- const response = await fetchLandingPageData(
- {
- startTime: '1',
- endTime: '2',
- bucketSize: '3',
- },
- { theme }
- );
+ const response = await fetchLandingPageData({
+ startTime: '1',
+ endTime: '2',
+ bucketSize: '3',
+ });
expect(response).toEqual({
title: 'APM',
appLink: '/app/apm',
stats: {
services: {
type: 'number',
- label: 'Services',
value: 0,
},
transactions: {
type: 'number',
- label: 'Transactions',
value: 0,
- color: '#6092c0',
},
},
series: {
transactions: {
- label: 'Transactions',
coordinates: [],
- color: '#6092c0',
},
},
});
@@ -122,35 +105,27 @@ describe('Observability dashboard data', () => {
transactionCoordinates: [{ x: 1 }, { x: 2 }, { x: 3 }],
})
);
- const response = await fetchLandingPageData(
- {
- startTime: '1',
- endTime: '2',
- bucketSize: '3',
- },
- { theme }
- );
+ const response = await fetchLandingPageData({
+ startTime: '1',
+ endTime: '2',
+ bucketSize: '3',
+ });
expect(response).toEqual({
title: 'APM',
appLink: '/app/apm',
stats: {
services: {
type: 'number',
- label: 'Services',
value: 0,
},
transactions: {
type: 'number',
- label: 'Transactions',
value: 0,
- color: '#6092c0',
},
},
series: {
transactions: {
- label: 'Transactions',
coordinates: [{ x: 1 }, { x: 2 }, { x: 3 }],
- color: '#6092c0',
},
},
});
diff --git a/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts b/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts
index 79ccf8dbd6f9b..409cec8b9ce10 100644
--- a/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts
+++ b/x-pack/plugins/apm/public/services/rest/observability_dashboard.ts
@@ -6,21 +6,17 @@
import { i18n } from '@kbn/i18n';
import { mean } from 'lodash';
-import { Theme } from '@kbn/ui-shared-deps/theme';
import {
ApmFetchDataResponse,
FetchDataParams,
} from '../../../../observability/public';
import { callApmApi } from './createCallApmApi';
-interface Options {
- theme: Theme;
-}
-
-export const fetchLandingPageData = async (
- { startTime, endTime, bucketSize }: FetchDataParams,
- { theme }: Options
-): Promise => {
+export const fetchLandingPageData = async ({
+ startTime,
+ endTime,
+ bucketSize,
+}: FetchDataParams): Promise => {
const data = await callApmApi({
pathname: '/api/apm/observability_dashboard',
params: { query: { start: startTime, end: endTime, bucketSize } },
@@ -36,34 +32,20 @@ export const fetchLandingPageData = async (
stats: {
services: {
type: 'number',
- label: i18n.translate(
- 'xpack.apm.observabilityDashboard.stats.services',
- { defaultMessage: 'Services' }
- ),
value: serviceCount,
},
transactions: {
type: 'number',
- label: i18n.translate(
- 'xpack.apm.observabilityDashboard.stats.transactions',
- { defaultMessage: 'Transactions' }
- ),
value:
mean(
transactionCoordinates
.map(({ y }) => y)
.filter((y) => y && isFinite(y))
) || 0,
- color: theme.euiColorVis1,
},
},
series: {
transactions: {
- label: i18n.translate(
- 'xpack.apm.observabilityDashboard.chart.transactions',
- { defaultMessage: 'Transactions' }
- ),
- color: theme.euiColorVis1,
coordinates: transactionCoordinates,
},
},
diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/constants.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/constants.ts
new file mode 100644
index 0000000000000..bfc4fcde09972
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/anomaly_detection/constants.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export const ML_MODULE_ID_APM_TRANSACTION = 'apm_transaction';
+export const APM_ML_JOB_GROUP = 'apm';
diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts
index 406097805775d..e723393a24013 100644
--- a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts
+++ b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts
@@ -6,6 +6,7 @@
import { Logger } from 'kibana/server';
import uuid from 'uuid/v4';
+import { snakeCase } from 'lodash';
import { PromiseReturnType } from '../../../../observability/typings/common';
import { Setup } from '../helpers/setup_request';
import {
@@ -14,9 +15,7 @@ import {
PROCESSOR_EVENT,
} from '../../../common/elasticsearch_fieldnames';
import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values';
-
-const ML_MODULE_ID_APM_TRANSACTION = 'apm_transaction';
-export const ML_GROUP_NAME_APM = 'apm';
+import { APM_ML_JOB_GROUP, ML_MODULE_ID_APM_TRANSACTION } from './constants';
export type CreateAnomalyDetectionJobsAPIResponse = PromiseReturnType<
typeof createAnomalyDetectionJobs
@@ -78,13 +77,12 @@ async function createAnomalyDetectionJob({
environment: string;
indexPatternName?: string | undefined;
}) {
- const convertedEnvironmentName = convertToMLIdentifier(environment);
const randomToken = uuid().substr(-4);
return ml.modules.setup({
moduleId: ML_MODULE_ID_APM_TRANSACTION,
- prefix: `${ML_GROUP_NAME_APM}-${convertedEnvironmentName}-${randomToken}-`,
- groups: [ML_GROUP_NAME_APM, convertedEnvironmentName],
+ prefix: `${APM_ML_JOB_GROUP}-${snakeCase(environment)}-${randomToken}-`,
+ groups: [APM_ML_JOB_GROUP],
indexPatternName,
query: {
bool: {
@@ -101,7 +99,11 @@ async function createAnomalyDetectionJob({
jobOverrides: [
{
custom_settings: {
- job_tags: { environment },
+ job_tags: {
+ environment,
+ // identifies this as an APM ML job & facilitates future migrations
+ apm_ml_version: 2,
+ },
},
},
],
@@ -117,7 +119,3 @@ const ENVIRONMENT_NOT_DEFINED_FILTER = {
},
},
};
-
-export function convertToMLIdentifier(value: string) {
- return value.replace(/\s+/g, '_').toLowerCase();
-}
diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts
index 252c87e9263db..8fdebeb597eaf 100644
--- a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts
+++ b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts
@@ -5,56 +5,34 @@
*/
import { Logger } from 'kibana/server';
-import { PromiseReturnType } from '../../../../observability/typings/common';
import { Setup } from '../helpers/setup_request';
-import { AnomalyDetectionJobByEnv } from '../../../typings/anomaly_detection';
-import { ML_GROUP_NAME_APM } from './create_anomaly_detection_jobs';
+import { getMlJobsWithAPMGroup } from './get_ml_jobs_by_group';
-export type AnomalyDetectionJobsAPIResponse = PromiseReturnType<
- typeof getAnomalyDetectionJobs
->;
-export async function getAnomalyDetectionJobs(
- setup: Setup,
- logger: Logger
-): Promise {
+export async function getAnomalyDetectionJobs(setup: Setup, logger: Logger) {
const { ml } = setup;
if (!ml) {
return [];
}
- try {
- const mlCapabilities = await ml.mlSystem.mlCapabilities();
- if (
- !(
- mlCapabilities.mlFeatureEnabledInSpace &&
- mlCapabilities.isPlatinumOrTrialLicense
- )
- ) {
- logger.warn(
- 'Anomaly detection integration is not availble for this user.'
- );
- return [];
- }
- } catch (error) {
- logger.warn('Unable to get ML capabilities.');
- logger.error(error);
- return [];
- }
- try {
- const { jobs } = await ml.anomalyDetectors.jobs(ML_GROUP_NAME_APM);
- return jobs
- .map((job) => {
- const environment = job.custom_settings?.job_tags?.environment ?? '';
- return {
- job_id: job.job_id,
- environment,
- };
- })
- .filter((job) => job.environment);
- } catch (error) {
- if (error.statusCode !== 404) {
- logger.warn('Unable to get APM ML jobs.');
- logger.error(error);
- }
+
+ const mlCapabilities = await ml.mlSystem.mlCapabilities();
+ if (
+ !(
+ mlCapabilities.mlFeatureEnabledInSpace &&
+ mlCapabilities.isPlatinumOrTrialLicense
+ )
+ ) {
+ logger.warn('Anomaly detection integration is not availble for this user.');
return [];
}
+
+ const response = await getMlJobsWithAPMGroup(ml);
+ return response.jobs
+ .filter((job) => (job.custom_settings?.job_tags?.apm_ml_version ?? 0) >= 2)
+ .map((job) => {
+ const environment = job.custom_settings?.job_tags?.environment ?? '';
+ return {
+ job_id: job.job_id,
+ environment,
+ };
+ });
}
diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/get_ml_jobs_by_group.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/get_ml_jobs_by_group.ts
new file mode 100644
index 0000000000000..5c0a3d17648aa
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/anomaly_detection/get_ml_jobs_by_group.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Setup } from '../helpers/setup_request';
+import { APM_ML_JOB_GROUP } from './constants';
+
+// returns ml jobs containing "apm" group
+// workaround: the ML api returns 404 when no jobs are found. This is handled so instead of throwing an empty response is returned
+export async function getMlJobsWithAPMGroup(ml: NonNullable) {
+ try {
+ return await ml.anomalyDetectors.jobs(APM_ML_JOB_GROUP);
+ } catch (e) {
+ if (e.statusCode === 404) {
+ return { count: 0, jobs: [] };
+ }
+
+ throw e;
+ }
+}
diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts
new file mode 100644
index 0000000000000..bf502607fcc1d
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { Setup } from '../helpers/setup_request';
+import { getMlJobsWithAPMGroup } from './get_ml_jobs_by_group';
+
+// Determine whether there are any legacy ml jobs.
+// A legacy ML job has a job id that ends with "high_mean_response_time" and created_by=ml-module-apm-transaction
+export async function hasLegacyJobs(setup: Setup) {
+ const { ml } = setup;
+
+ if (!ml) {
+ return false;
+ }
+
+ const response = await getMlJobsWithAPMGroup(ml);
+ return response.jobs.some(
+ (job) =>
+ job.job_id.endsWith('high_mean_response_time') &&
+ job.custom_settings?.created_by === 'ml-module-apm-transaction'
+ );
+}
diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts
index c648cf4cc116a..e3161b49b315d 100644
--- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts
+++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.test.ts
@@ -65,4 +65,40 @@ describe('data telemetry collection tasks', () => {
});
});
});
+
+ describe('integrations', () => {
+ const integrationsTask = tasks.find((task) => task.name === 'integrations');
+
+ it('returns the count of ML jobs', async () => {
+ const transportRequest = jest
+ .fn()
+ .mockResolvedValueOnce({ body: { count: 1 } });
+
+ expect(
+ await integrationsTask?.executor({ indices, transportRequest } as any)
+ ).toEqual({
+ integrations: {
+ ml: {
+ all_jobs_count: 1,
+ },
+ },
+ });
+ });
+
+ describe('with no data', () => {
+ it('returns a count of 0', async () => {
+ const transportRequest = jest.fn().mockResolvedValueOnce({});
+
+ expect(
+ await integrationsTask?.executor({ indices, transportRequest } as any)
+ ).toEqual({
+ integrations: {
+ ml: {
+ all_jobs_count: 0,
+ },
+ },
+ });
+ });
+ });
+ });
});
diff --git a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts
index f27af9a2cc516..4bbaaf3e86e78 100644
--- a/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts
+++ b/x-pack/plugins/apm/server/lib/apm_telemetry/collect_data_telemetry/tasks.ts
@@ -4,31 +4,31 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { flatten, merge, sortBy, sum } from 'lodash';
-import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent';
+import { TelemetryTask } from '.';
import { AGENT_NAMES } from '../../../../common/agent_name';
-import { Transaction } from '../../../../typings/es_schemas/ui/transaction';
import {
- PROCESSOR_EVENT,
- SERVICE_NAME,
AGENT_NAME,
AGENT_VERSION,
+ CLOUD_AVAILABILITY_ZONE,
+ CLOUD_PROVIDER,
+ CLOUD_REGION,
ERROR_GROUP_ID,
- TRANSACTION_NAME,
PARENT_ID,
+ PROCESSOR_EVENT,
SERVICE_FRAMEWORK_NAME,
SERVICE_FRAMEWORK_VERSION,
SERVICE_LANGUAGE_NAME,
SERVICE_LANGUAGE_VERSION,
+ SERVICE_NAME,
SERVICE_RUNTIME_NAME,
SERVICE_RUNTIME_VERSION,
+ TRANSACTION_NAME,
USER_AGENT_ORIGINAL,
- CLOUD_AVAILABILITY_ZONE,
- CLOUD_PROVIDER,
- CLOUD_REGION,
} from '../../../../common/elasticsearch_fieldnames';
-import { Span } from '../../../../typings/es_schemas/ui/span';
import { APMError } from '../../../../typings/es_schemas/ui/apm_error';
-import { TelemetryTask } from '.';
+import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent';
+import { Span } from '../../../../typings/es_schemas/ui/span';
+import { Transaction } from '../../../../typings/es_schemas/ui/transaction';
import { APMTelemetry } from '../types';
const TIME_RANGES = ['1d', 'all'] as const;
@@ -465,17 +465,17 @@ export const tasks: TelemetryTask[] = [
{
name: 'integrations',
executor: async ({ transportRequest }) => {
- const apmJobs = ['*-high_mean_response_time'];
+ const apmJobs = ['apm-*', '*-high_mean_response_time'];
const response = (await transportRequest({
method: 'get',
path: `/_ml/anomaly_detectors/${apmJobs.join(',')}`,
- })) as { data?: { count: number } };
+ })) as { body?: { count: number } };
return {
integrations: {
ml: {
- all_jobs_count: response.data?.count ?? 0,
+ all_jobs_count: response.body?.count ?? 0,
},
},
};
diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts
new file mode 100644
index 0000000000000..3e5ef5eb37b02
--- /dev/null
+++ b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts
@@ -0,0 +1,166 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { Logger } from 'kibana/server';
+import { Setup, SetupTimeRange } from '../helpers/setup_request';
+import { PromiseReturnType } from '../../../typings/common';
+import {
+ TRANSACTION_PAGE_LOAD,
+ TRANSACTION_REQUEST,
+} from '../../../common/transaction_types';
+import { ServiceAnomalyStats } from '../../../common/anomaly_detection';
+import { APM_ML_JOB_GROUP } from '../anomaly_detection/constants';
+
+export const DEFAULT_ANOMALIES = { mlJobIds: [], serviceAnomalies: {} };
+
+export type ServiceAnomaliesResponse = PromiseReturnType<
+ typeof getServiceAnomalies
+>;
+
+export async function getServiceAnomalies({
+ setup,
+ logger,
+ environment,
+}: {
+ setup: Setup & SetupTimeRange;
+ logger: Logger;
+ environment?: string;
+}) {
+ const { ml, start, end } = setup;
+
+ if (!ml) {
+ logger.warn('Anomaly detection plugin is not available.');
+ return DEFAULT_ANOMALIES;
+ }
+ const mlCapabilities = await ml.mlSystem.mlCapabilities();
+ if (!mlCapabilities.mlFeatureEnabledInSpace) {
+ logger.warn('Anomaly detection feature is not enabled for the space.');
+ return DEFAULT_ANOMALIES;
+ }
+ if (!mlCapabilities.isPlatinumOrTrialLicense) {
+ logger.warn(
+ 'Unable to create anomaly detection jobs due to insufficient license.'
+ );
+ return DEFAULT_ANOMALIES;
+ }
+
+ let mlJobIds: string[] = [];
+ try {
+ mlJobIds = await getMLJobIds(ml, environment);
+ } catch (error) {
+ logger.error(error);
+ return DEFAULT_ANOMALIES;
+ }
+
+ const params = {
+ body: {
+ size: 0,
+ query: {
+ bool: {
+ filter: [
+ { term: { result_type: 'record' } },
+ { terms: { job_id: mlJobIds } },
+ {
+ range: {
+ timestamp: { gte: start, lte: end, format: 'epoch_millis' },
+ },
+ },
+ {
+ terms: {
+ // Only retrieving anomalies for transaction types "request" and "page-load"
+ by_field_value: [TRANSACTION_REQUEST, TRANSACTION_PAGE_LOAD],
+ },
+ },
+ ],
+ },
+ },
+ aggs: {
+ services: {
+ terms: { field: 'partition_field_value' },
+ aggs: {
+ top_score: {
+ top_hits: {
+ sort: { record_score: 'desc' },
+ _source: { includes: ['actual', 'job_id', 'by_field_value'] },
+ size: 1,
+ },
+ },
+ },
+ },
+ },
+ },
+ };
+ const response = await ml.mlSystem.mlAnomalySearch(params);
+ return {
+ mlJobIds,
+ serviceAnomalies: transformResponseToServiceAnomalies(
+ response as ServiceAnomaliesAggResponse
+ ),
+ };
+}
+
+interface ServiceAnomaliesAggResponse {
+ aggregations: {
+ services: {
+ buckets: Array<{
+ key: string;
+ top_score: {
+ hits: {
+ hits: Array<{
+ sort: [number];
+ _source: {
+ actual: [number];
+ job_id: string;
+ by_field_value: string;
+ };
+ }>;
+ };
+ };
+ }>;
+ };
+ };
+}
+
+function transformResponseToServiceAnomalies(
+ response: ServiceAnomaliesAggResponse
+): Record {
+ const serviceAnomaliesMap = response.aggregations.services.buckets.reduce(
+ (statsByServiceName, { key: serviceName, top_score: topScoreAgg }) => {
+ return {
+ ...statsByServiceName,
+ [serviceName]: {
+ transactionType: topScoreAgg.hits.hits[0]?._source?.by_field_value,
+ anomalyScore: topScoreAgg.hits.hits[0]?.sort?.[0],
+ actualValue: topScoreAgg.hits.hits[0]?._source?.actual?.[0],
+ jobId: topScoreAgg.hits.hits[0]?._source?.job_id,
+ },
+ };
+ },
+ {}
+ );
+ return serviceAnomaliesMap;
+}
+
+export async function getMLJobIds(
+ ml: Required['ml'],
+ environment?: string
+) {
+ const response = await ml.anomalyDetectors.jobs(APM_ML_JOB_GROUP);
+ // to filter out legacy jobs we are filtering by the existence of `apm_ml_version` in `custom_settings`
+ // and checking that it is compatable.
+ const mlJobs = response.jobs.filter(
+ (job) => (job.custom_settings?.job_tags?.apm_ml_version ?? 0) >= 2
+ );
+ if (environment) {
+ const matchingMLJob = mlJobs.find(
+ (job) => job.custom_settings?.job_tags?.environment === environment
+ );
+ if (!matchingMLJob) {
+ throw new Error(`ML job Not Found for environment "${environment}".`);
+ }
+ return [matchingMLJob.job_id];
+ }
+ return mlJobs.map((job) => job.job_id);
+}
diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts
index 4d488cd1a5509..ea2bb14efdfc7 100644
--- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts
+++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { chunk } from 'lodash';
+import { Logger } from 'kibana/server';
import {
AGENT_NAME,
SERVICE_ENVIRONMENT,
@@ -16,11 +17,17 @@ import { Setup, SetupTimeRange } from '../helpers/setup_request';
import { transformServiceMapResponses } from './transform_service_map_responses';
import { getServiceMapFromTraceIds } from './get_service_map_from_trace_ids';
import { getTraceSampleIds } from './get_trace_sample_ids';
+import {
+ getServiceAnomalies,
+ ServiceAnomaliesResponse,
+ DEFAULT_ANOMALIES,
+} from './get_service_anomalies';
export interface IEnvOptions {
setup: Setup & SetupTimeRange;
serviceName?: string;
environment?: string;
+ logger: Logger;
}
async function getConnectionData({
@@ -132,13 +139,23 @@ export type ServicesResponse = PromiseReturnType;
export type ServiceMapAPIResponse = PromiseReturnType;
export async function getServiceMap(options: IEnvOptions) {
- const [connectionData, servicesData] = await Promise.all([
+ const { logger } = options;
+ const anomaliesPromise: Promise = getServiceAnomalies(
+ options
+ ).catch((error) => {
+ logger.warn(`Unable to retrieve anomalies for service maps.`);
+ logger.error(error);
+ return DEFAULT_ANOMALIES;
+ });
+ const [connectionData, servicesData, anomalies] = await Promise.all([
getConnectionData(options),
getServicesData(options),
+ anomaliesPromise,
]);
return transformServiceMapResponses({
...connectionData,
services: servicesData,
+ anomalies,
});
}
diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts
index e521efa687388..be92bfe5a0099 100644
--- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts
+++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts
@@ -12,11 +12,17 @@ import {
SERVICE_ENVIRONMENT,
SERVICE_NAME,
TRANSACTION_DURATION,
+ TRANSACTION_TYPE,
METRIC_SYSTEM_CPU_PERCENT,
METRIC_SYSTEM_FREE_MEMORY,
METRIC_SYSTEM_TOTAL_MEMORY,
} from '../../../common/elasticsearch_fieldnames';
import { percentMemoryUsedScript } from '../metrics/by_agent/shared/memory';
+import {
+ TRANSACTION_REQUEST,
+ TRANSACTION_PAGE_LOAD,
+} from '../../../common/transaction_types';
+import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values';
interface Options {
setup: Setup & SetupTimeRange;
@@ -37,12 +43,23 @@ export async function getServiceMapServiceNodeInfo({
}: Options & { serviceName: string; environment?: string }) {
const { start, end } = setup;
+ const environmentNotDefinedFilter = {
+ bool: { must_not: [{ exists: { field: SERVICE_ENVIRONMENT } }] },
+ };
+
const filter: ESFilter[] = [
{ range: rangeFilter(start, end) },
{ term: { [SERVICE_NAME]: serviceName } },
- ...(environment ? [{ term: { [SERVICE_ENVIRONMENT]: environment } }] : []),
];
+ if (environment) {
+ filter.push(
+ environment === ENVIRONMENT_NOT_DEFINED
+ ? environmentNotDefinedFilter
+ : { term: { [SERVICE_ENVIRONMENT]: environment } }
+ );
+ }
+
const minutes = Math.abs((end - start) / (1000 * 60));
const taskParams = {
@@ -53,19 +70,19 @@ export async function getServiceMapServiceNodeInfo({
const [
errorMetrics,
- transactionMetrics,
+ transactionStats,
cpuMetrics,
memoryMetrics,
] = await Promise.all([
getErrorMetrics(taskParams),
- getTransactionMetrics(taskParams),
+ getTransactionStats(taskParams),
getCpuMetrics(taskParams),
getMemoryMetrics(taskParams),
]);
return {
...errorMetrics,
- ...transactionMetrics,
+ transactionStats,
...cpuMetrics,
...memoryMetrics,
};
@@ -99,7 +116,7 @@ async function getErrorMetrics({ setup, minutes, filter }: TaskParameters) {
};
}
-async function getTransactionMetrics({
+async function getTransactionStats({
setup,
filter,
minutes,
@@ -109,17 +126,28 @@ async function getTransactionMetrics({
}> {
const { indices, client } = setup;
- const response = await client.search({
+ const params = {
index: indices['apm_oss.transactionIndices'],
body: {
- size: 1,
+ size: 0,
query: {
bool: {
- filter: filter.concat({
- term: {
- [PROCESSOR_EVENT]: 'transaction',
+ filter: [
+ ...filter,
+ {
+ term: {
+ [PROCESSOR_EVENT]: 'transaction',
+ },
},
- }),
+ {
+ terms: {
+ [TRANSACTION_TYPE]: [
+ TRANSACTION_REQUEST,
+ TRANSACTION_PAGE_LOAD,
+ ],
+ },
+ },
+ ],
},
},
track_total_hits: true,
@@ -131,14 +159,12 @@ async function getTransactionMetrics({
},
},
},
- });
-
+ };
+ const response = await client.search(params);
+ const docCount = response.hits.total.value;
return {
avgTransactionDuration: response.aggregations?.duration.value ?? null,
- avgRequestsPerMinute:
- response.hits.total.value > 0
- ? response.hits.total.value / minutes
- : null,
+ avgRequestsPerMinute: docCount > 0 ? docCount / minutes : null,
};
}
diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts
index 1e26634bdf0f1..7e4bcfdda7382 100644
--- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts
+++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.test.ts
@@ -35,6 +35,18 @@ const javaService = {
[AGENT_NAME]: 'java',
};
+const anomalies = {
+ mlJobIds: ['apm-test-1234-ml-module-name'],
+ serviceAnomalies: {
+ 'opbeans-test': {
+ transactionType: 'request',
+ actualValue: 10000,
+ anomalyScore: 50,
+ jobId: 'apm-test-1234-ml-module-name',
+ },
+ },
+};
+
describe('transformServiceMapResponses', () => {
it('maps external destinations to internal services', () => {
const response: ServiceMapResponse = {
@@ -51,6 +63,7 @@ describe('transformServiceMapResponses', () => {
destination: nodejsExternal,
},
],
+ anomalies,
};
const { elements } = transformServiceMapResponses(response);
@@ -89,6 +102,7 @@ describe('transformServiceMapResponses', () => {
},
},
],
+ anomalies,
};
const { elements } = transformServiceMapResponses(response);
@@ -126,6 +140,7 @@ describe('transformServiceMapResponses', () => {
},
},
],
+ anomalies,
};
const { elements } = transformServiceMapResponses(response);
@@ -150,6 +165,7 @@ describe('transformServiceMapResponses', () => {
destination: nodejsService,
},
],
+ anomalies,
};
const { elements } = transformServiceMapResponses(response);
diff --git a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts
index 2e394f44b25b1..7f5e34f68f922 100644
--- a/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts
+++ b/x-pack/plugins/apm/server/lib/service_map/transform_service_map_responses.ts
@@ -18,6 +18,7 @@ import {
ExternalConnectionNode,
} from '../../../common/service_map';
import { ConnectionsResponse, ServicesResponse } from './get_service_map';
+import { ServiceAnomaliesResponse } from './get_service_anomalies';
function getConnectionNodeId(node: ConnectionNode): string {
if ('span.destination.service.resource' in node) {
@@ -63,10 +64,11 @@ export function getServiceNodes(allNodes: ConnectionNode[]) {
export type ServiceMapResponse = ConnectionsResponse & {
services: ServicesResponse;
+ anomalies: ServiceAnomaliesResponse;
};
export function transformServiceMapResponses(response: ServiceMapResponse) {
- const { discoveredServices, services, connections } = response;
+ const { discoveredServices, services, connections, anomalies } = response;
const allNodes = getAllNodes(services, connections);
const serviceNodes = getServiceNodes(allNodes);
@@ -100,21 +102,23 @@ export function transformServiceMapResponses(response: ServiceMapResponse) {
serviceName = node[SERVICE_NAME];
}
- const matchedServiceNodes = serviceNodes.filter(
- (serviceNode) => serviceNode[SERVICE_NAME] === serviceName
- );
+ const matchedServiceNodes = serviceNodes
+ .filter((serviceNode) => serviceNode[SERVICE_NAME] === serviceName)
+ .map((serviceNode) => pickBy(serviceNode, identity));
+ const mergedServiceNode = Object.assign({}, ...matchedServiceNodes);
+
+ const serviceAnomalyStats = serviceName
+ ? anomalies.serviceAnomalies[serviceName]
+ : null;
if (matchedServiceNodes.length) {
return {
...map,
- [node.id]: Object.assign(
- {
- id: matchedServiceNodes[0][SERVICE_NAME],
- },
- ...matchedServiceNodes.map((serviceNode) =>
- pickBy(serviceNode, identity)
- )
- ),
+ [node.id]: {
+ id: matchedServiceNodes[0][SERVICE_NAME],
+ ...mergedServiceNode,
+ ...(serviceAnomalyStats ? { serviceAnomalyStats } : null),
+ },
};
}
diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts
index a3e2f708b0b22..50123131a42e7 100644
--- a/x-pack/plugins/apm/server/routes/service_map.ts
+++ b/x-pack/plugins/apm/server/routes/service_map.ts
@@ -37,11 +37,12 @@ export const serviceMapRoute = createRoute(() => ({
}
context.licensing.featureUsage.notifyUsage(APM_SERVICE_MAPS_FEATURE_NAME);
+ const logger = context.logger;
const setup = await setupRequest(context, request);
const {
query: { serviceName, environment },
} = context.params;
- return getServiceMap({ setup, serviceName, environment });
+ return getServiceMap({ setup, serviceName, environment, logger });
},
}));
diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts
index 67eca0da946d0..7009470e1ff17 100644
--- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts
+++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts
@@ -10,6 +10,7 @@ import { getAnomalyDetectionJobs } from '../../lib/anomaly_detection/get_anomaly
import { createAnomalyDetectionJobs } from '../../lib/anomaly_detection/create_anomaly_detection_jobs';
import { setupRequest } from '../../lib/helpers/setup_request';
import { getAllEnvironments } from '../../lib/environments/get_all_environments';
+import { hasLegacyJobs } from '../../lib/anomaly_detection/has_legacy_jobs';
// get ML anomaly detection jobs for each environment
export const anomalyDetectionJobsRoute = createRoute(() => ({
@@ -17,7 +18,11 @@ export const anomalyDetectionJobsRoute = createRoute(() => ({
path: '/api/apm/settings/anomaly-detection',
handler: async ({ context, request }) => {
const setup = await setupRequest(context, request);
- return await getAnomalyDetectionJobs(setup, context.logger);
+ const jobs = await getAnomalyDetectionJobs(setup, context.logger);
+ return {
+ jobs,
+ hasLegacyJobs: await hasLegacyJobs(setup),
+ };
},
}));
diff --git a/x-pack/plugins/canvas/.storybook/storyshots.test.js b/x-pack/plugins/canvas/.storybook/storyshots.test.js
index e3217ad4dbe58..a3412c3a14e79 100644
--- a/x-pack/plugins/canvas/.storybook/storyshots.test.js
+++ b/x-pack/plugins/canvas/.storybook/storyshots.test.js
@@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import fs from 'fs';
import path from 'path';
import moment from 'moment';
import 'moment-timezone';
@@ -64,6 +63,14 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => {
};
});
+// To be resolved by EUI team.
+// https://github.com/elastic/eui/issues/3712
+jest.mock('@elastic/eui/lib/components/overlay_mask/overlay_mask', () => {
+ return {
+ EuiOverlayMask: ({children}) => children,
+ };
+});
+
// Disabling this test due to https://github.com/elastic/eui/issues/2242
jest.mock(
'../public/components/workpad_header/share_menu/flyout/__examples__/share_website_flyout.stories',
@@ -77,12 +84,6 @@ import { RenderedElement } from '../shareable_runtime/components/rendered_elemen
jest.mock('../shareable_runtime/components/rendered_element');
RenderedElement.mockImplementation(() => 'RenderedElement');
-// Some of the code requires that this directory exists, but the tests don't actually require any css to be present
-const cssDir = path.resolve(__dirname, '../../../../built_assets/css');
-if (!fs.existsSync(cssDir)) {
- fs.mkdirSync(cssDir, { recursive: true });
-}
-
addSerializer(styleSheetSerializer);
// Initialize Storyshots and build the Jest Snapshots
diff --git a/x-pack/plugins/canvas/kibana.json b/x-pack/plugins/canvas/kibana.json
index 2d6ab43228aa1..5f4ea5802cb13 100644
--- a/x-pack/plugins/canvas/kibana.json
+++ b/x-pack/plugins/canvas/kibana.json
@@ -6,5 +6,6 @@
"server": true,
"ui": true,
"requiredPlugins": ["data", "embeddable", "expressions", "features", "home", "inspector", "uiActions"],
- "optionalPlugins": ["usageCollection"]
+ "optionalPlugins": ["usageCollection"],
+ "requiredBundles": ["kibanaReact", "maps", "lens", "visualizations", "kibanaUtils", "kibanaLegacy", "discover", "savedObjects", "reporting"]
}
diff --git a/x-pack/plugins/canvas/public/components/autocomplete/autocomplete.js b/x-pack/plugins/canvas/public/components/autocomplete/autocomplete.js
index 8afa5d16b59fd..7dc8b762359f9 100644
--- a/x-pack/plugins/canvas/public/components/autocomplete/autocomplete.js
+++ b/x-pack/plugins/canvas/public/components/autocomplete/autocomplete.js
@@ -15,7 +15,7 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { EuiFlexGroup, EuiFlexItem, EuiPanel, keyCodes } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, EuiPanel, keys } from '@elastic/eui';
/**
* An autocomplete component. Currently this is only used for the expression editor but in theory
@@ -134,27 +134,27 @@ export class Autocomplete extends React.Component {
* the item selection, closing the menu, etc.
*/
onKeyDown = (e) => {
- const { ESCAPE, TAB, ENTER, UP, DOWN, LEFT, RIGHT } = keyCodes;
- const { keyCode } = e;
+ const { BACKSPACE, ESCAPE, TAB, ENTER, ARROW_UP, ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT } = keys;
+ const { key } = e;
const { items } = this.props;
const { isOpen, selectedIndex } = this.state;
- if ([ESCAPE, LEFT, RIGHT].includes(keyCode)) {
+ if ([ESCAPE, ARROW_LEFT, ARROW_RIGHT].includes(key)) {
this.setState({ isOpen: false });
}
- if ([TAB, ENTER].includes(keyCode) && isOpen && selectedIndex >= 0) {
+ if ([TAB, ENTER].includes(key) && isOpen && selectedIndex >= 0) {
e.preventDefault();
this.onSubmit();
- } else if (keyCode === UP && items.length > 0 && isOpen) {
+ } else if (key === ARROW_UP && items.length > 0 && isOpen) {
e.preventDefault();
this.selectPrevious();
- } else if (keyCode === DOWN && items.length > 0 && isOpen) {
+ } else if (key === ARROW_DOWN && items.length > 0 && isOpen) {
e.preventDefault();
this.selectNext();
- } else if (e.key === 'Backspace') {
+ } else if (key === BACKSPACE) {
this.setState({ isOpen: true });
- } else if (!['Shift', 'Control', 'Alt', 'Meta'].includes(e.key)) {
+ } else if (!['Shift', 'Control', 'Alt', 'Meta'].includes(key)) {
this.setState({ selectedIndex: -1 });
}
};
diff --git a/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot
index ea31d1daa97ca..97d13dcd69830 100644
--- a/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot
+++ b/x-pack/plugins/canvas/public/components/workpad_templates/examples/__snapshots__/workpad_templates.stories.storyshot
@@ -156,6 +156,8 @@ exports[`Storyshots components/WorkpadTemplates default 1`] = `
-
-
-
-
- 1
-
-
-
+
+
+ 1
+
+
+
+
+
-
+
diff --git a/x-pack/plugins/case/common/api/cases/configure.ts b/x-pack/plugins/case/common/api/cases/configure.ts
index 7d20011a428cf..38fff5b190f25 100644
--- a/x-pack/plugins/case/common/api/cases/configure.ts
+++ b/x-pack/plugins/case/common/api/cases/configure.ts
@@ -10,6 +10,7 @@ import { ActionResult } from '../../../../actions/common';
import { UserRT } from '../user';
import { JiraFieldsRT } from '../connectors/jira';
import { ServiceNowFieldsRT } from '../connectors/servicenow';
+import { ResilientFieldsRT } from '../connectors/resilient';
/*
* This types below are related to the service now configuration
@@ -29,7 +30,12 @@ const CaseFieldRT = rt.union([
rt.literal('comments'),
]);
-const ThirdPartyFieldRT = rt.union([JiraFieldsRT, ServiceNowFieldsRT, rt.literal('not_mapped')]);
+const ThirdPartyFieldRT = rt.union([
+ JiraFieldsRT,
+ ServiceNowFieldsRT,
+ ResilientFieldsRT,
+ rt.literal('not_mapped'),
+]);
export const CasesConfigurationMapsRT = rt.type({
source: CaseFieldRT,
diff --git a/x-pack/plugins/case/common/api/connectors/index.ts b/x-pack/plugins/case/common/api/connectors/index.ts
index c1fc284c938b7..0a7840d3aba22 100644
--- a/x-pack/plugins/case/common/api/connectors/index.ts
+++ b/x-pack/plugins/case/common/api/connectors/index.ts
@@ -6,3 +6,4 @@
export * from './jira';
export * from './servicenow';
+export * from './resilient';
diff --git a/x-pack/plugins/case/common/api/connectors/resilient.ts b/x-pack/plugins/case/common/api/connectors/resilient.ts
new file mode 100644
index 0000000000000..c7e2f19809140
--- /dev/null
+++ b/x-pack/plugins/case/common/api/connectors/resilient.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import * as rt from 'io-ts';
+
+export const ResilientFieldsRT = rt.union([
+ rt.literal('name'),
+ rt.literal('description'),
+ rt.literal('comments'),
+]);
+
+export type ResilientFieldsType = rt.TypeOf;
diff --git a/x-pack/plugins/case/common/constants.ts b/x-pack/plugins/case/common/constants.ts
index e912c661439b2..bd12c258a5388 100644
--- a/x-pack/plugins/case/common/constants.ts
+++ b/x-pack/plugins/case/common/constants.ts
@@ -29,4 +29,4 @@ export const ACTION_URL = '/api/actions';
export const ACTION_TYPES_URL = '/api/actions/list_action_types';
export const SERVICENOW_ACTION_TYPE_ID = '.servicenow';
-export const SUPPORTED_CONNECTORS = ['.servicenow', '.jira'];
+export const SUPPORTED_CONNECTORS = ['.servicenow', '.jira', '.resilient'];
diff --git a/x-pack/plugins/cross_cluster_replication/kibana.json b/x-pack/plugins/cross_cluster_replication/kibana.json
index ccf98f41def47..13746bb0e34c3 100644
--- a/x-pack/plugins/cross_cluster_replication/kibana.json
+++ b/x-pack/plugins/cross_cluster_replication/kibana.json
@@ -13,5 +13,10 @@
"optionalPlugins": [
"usageCollection"
],
- "configPath": ["xpack", "ccr"]
+ "configPath": ["xpack", "ccr"],
+ "requiredBundles": [
+ "kibanaReact",
+ "esUiShared",
+ "data"
+ ]
}
diff --git a/x-pack/plugins/dashboard_enhanced/kibana.json b/x-pack/plugins/dashboard_enhanced/kibana.json
index 3a95419d2f2fe..ba5d8052ca787 100644
--- a/x-pack/plugins/dashboard_enhanced/kibana.json
+++ b/x-pack/plugins/dashboard_enhanced/kibana.json
@@ -4,5 +4,10 @@
"server": false,
"ui": true,
"requiredPlugins": ["data", "uiActionsEnhanced", "embeddable", "dashboard", "share"],
- "configPath": ["xpack", "dashboardEnhanced"]
+ "configPath": ["xpack", "dashboardEnhanced"],
+ "requiredBundles": [
+ "kibanaUtils",
+ "embeddableEnhanced",
+ "kibanaReact"
+ ]
}
diff --git a/x-pack/plugins/data_enhanced/kibana.json b/x-pack/plugins/data_enhanced/kibana.json
index 1be55d2b7a635..f0baa84afca32 100644
--- a/x-pack/plugins/data_enhanced/kibana.json
+++ b/x-pack/plugins/data_enhanced/kibana.json
@@ -10,5 +10,6 @@
],
"optionalPlugins": ["kibanaReact", "kibanaUtils"],
"server": true,
- "ui": true
+ "ui": true,
+ "requiredBundles": ["kibanaReact", "kibanaUtils"]
}
diff --git a/x-pack/plugins/discover_enhanced/kibana.json b/x-pack/plugins/discover_enhanced/kibana.json
index 704096ce7fcad..fbd04fe009687 100644
--- a/x-pack/plugins/discover_enhanced/kibana.json
+++ b/x-pack/plugins/discover_enhanced/kibana.json
@@ -6,5 +6,6 @@
"ui": true,
"requiredPlugins": ["uiActions", "embeddable", "discover"],
"optionalPlugins": ["share"],
- "configPath": ["xpack", "discoverEnhanced"]
+ "configPath": ["xpack", "discoverEnhanced"],
+ "requiredBundles": ["kibanaUtils", "data"]
}
diff --git a/x-pack/plugins/enterprise_search/README.md b/x-pack/plugins/enterprise_search/README.md
new file mode 100644
index 0000000000000..8c316c848184b
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/README.md
@@ -0,0 +1,25 @@
+# Enterprise Search
+
+## Overview
+
+This plugin's goal is to provide a Kibana user interface to the Enterprise Search solution's products (App Search and Workplace Search). In its current MVP state, the plugin provides a basic engines overview from App Search with the goal of gathering user feedback and raising product awareness.
+
+## Development
+
+1. When developing locally, Enterprise Search should be running locally alongside Kibana on `localhost:3002`.
+2. Update `config/kibana.dev.yml` with `enterpriseSearch.host: 'http://localhost:3002'`
+3. For faster QA/development, run Enterprise Search on [elasticsearch-native auth](https://www.elastic.co/guide/en/app-search/current/security-and-users.html#app-search-self-managed-security-and-user-management-elasticsearch-native-realm) and log in as the `elastic` superuser on Kibana.
+
+## Testing
+
+### Unit tests
+
+From `kibana-root-folder/x-pack`, run:
+
+```bash
+yarn test:jest plugins/enterprise_search
+```
+
+### E2E tests
+
+See [our functional test runner README](../../test/functional_enterprise_search).
diff --git a/x-pack/plugins/enterprise_search/common/constants.ts b/x-pack/plugins/enterprise_search/common/constants.ts
new file mode 100644
index 0000000000000..c134131caba75
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/common/constants.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export const ENGINES_PAGE_SIZE = 10;
diff --git a/x-pack/plugins/enterprise_search/kibana.json b/x-pack/plugins/enterprise_search/kibana.json
new file mode 100644
index 0000000000000..9a2daefcd8c6e
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/kibana.json
@@ -0,0 +1,10 @@
+{
+ "id": "enterpriseSearch",
+ "version": "kibana",
+ "kibanaVersion": "kibana",
+ "requiredPlugins": ["home", "features", "licensing"],
+ "configPath": ["enterpriseSearch"],
+ "optionalPlugins": ["usageCollection", "security"],
+ "server": true,
+ "ui": true
+}
diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts
new file mode 100644
index 0000000000000..14fde357a980a
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/index.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { mockHistory } from './react_router_history.mock';
+export { mockKibanaContext } from './kibana_context.mock';
+export { mockLicenseContext } from './license_context.mock';
+export { mountWithContext, mountWithKibanaContext } from './mount_with_context.mock';
+export { shallowWithIntl } from './shallow_with_i18n.mock';
+
+// Note: shallow_usecontext must be imported directly as a file
diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts
new file mode 100644
index 0000000000000..fcfa1b0a21f13
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_context.mock.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { httpServiceMock } from 'src/core/public/mocks';
+
+/**
+ * A set of default Kibana context values to use across component tests.
+ * @see enterprise_search/public/index.tsx for the KibanaContext definition/import
+ */
+export const mockKibanaContext = {
+ http: httpServiceMock.createSetupContract(),
+ setBreadcrumbs: jest.fn(),
+ enterpriseSearchUrl: 'http://localhost:3002',
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/license_context.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/license_context.mock.ts
new file mode 100644
index 0000000000000..7c37ecc7cde1b
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/license_context.mock.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { licensingMock } from '../../../../licensing/public/mocks';
+
+export const mockLicenseContext = {
+ license: licensingMock.createLicense(),
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx
new file mode 100644
index 0000000000000..dfcda544459d4
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_context.mock.tsx
@@ -0,0 +1,49 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { mount } from 'enzyme';
+
+import { I18nProvider } from '@kbn/i18n/react';
+import { KibanaContext } from '../';
+import { mockKibanaContext } from './kibana_context.mock';
+import { LicenseContext } from '../shared/licensing';
+import { mockLicenseContext } from './license_context.mock';
+
+/**
+ * This helper mounts a component with all the contexts/providers used
+ * by the production app, while allowing custom context to be
+ * passed in via a second arg
+ *
+ * Example usage:
+ *
+ * const wrapper = mountWithContext( , { enterpriseSearchUrl: 'someOverride', license: {} });
+ */
+export const mountWithContext = (children: React.ReactNode, context?: object) => {
+ return mount(
+
+
+
+ {children}
+
+
+
+ );
+};
+
+/**
+ * This helper mounts a component with just the default KibanaContext -
+ * useful for isolated / helper components that only need this context
+ *
+ * Same usage/override functionality as mountWithContext
+ */
+export const mountWithKibanaContext = (children: React.ReactNode, context?: object) => {
+ return mount(
+
+ {children}
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts
new file mode 100644
index 0000000000000..fd422465d87f1
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+/**
+ * NOTE: This variable name MUST start with 'mock*' in order for
+ * Jest to accept its use within a jest.mock()
+ */
+export const mockHistory = {
+ createHref: jest.fn(({ pathname }) => `/enterprise_search${pathname}`),
+ push: jest.fn(),
+ location: {
+ pathname: '/current-path',
+ },
+};
+
+jest.mock('react-router-dom', () => ({
+ useHistory: jest.fn(() => mockHistory),
+}));
+
+/**
+ * For example usage, @see public/applications/shared/react_router_helpers/eui_link.test.tsx
+ */
diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts
new file mode 100644
index 0000000000000..767a52a75d1fb
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_usecontext.mock.ts
@@ -0,0 +1,40 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+/**
+ * NOTE: These variable names MUST start with 'mock*' in order for
+ * Jest to accept its use within a jest.mock()
+ */
+import { mockKibanaContext } from './kibana_context.mock';
+import { mockLicenseContext } from './license_context.mock';
+
+jest.mock('react', () => ({
+ ...(jest.requireActual('react') as object),
+ useContext: jest.fn(() => ({ ...mockKibanaContext, ...mockLicenseContext })),
+}));
+
+/**
+ * Example usage within a component test using shallow():
+ *
+ * import '../../../test_utils/mock_shallow_usecontext'; // Must come before React's import, adjust relative path as needed
+ *
+ * import React from 'react';
+ * import { shallow } from 'enzyme';
+ *
+ * // ... etc.
+ */
+
+/**
+ * If you need to override the default mock context values, you can do so via jest.mockImplementation:
+ *
+ * import React, { useContext } from 'react';
+ *
+ * // ... etc.
+ *
+ * it('some test', () => {
+ * useContext.mockImplementationOnce(() => ({ enterpriseSearchUrl: 'someOverride' }));
+ * });
+ */
diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_with_i18n.mock.tsx b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_with_i18n.mock.tsx
new file mode 100644
index 0000000000000..ae7d0b09f9872
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_with_i18n.mock.tsx
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { shallow } from 'enzyme';
+import { I18nProvider } from '@kbn/i18n/react';
+import { IntlProvider } from 'react-intl';
+
+const intlProvider = new IntlProvider({ locale: 'en', messages: {} }, {});
+const { intl } = intlProvider.getChildContext();
+
+/**
+ * This helper shallow wraps a component with react-intl's which
+ * fixes "Could not find required `intl` object" console errors when running tests
+ *
+ * Example usage (should be the same as shallow()):
+ *
+ * const wrapper = shallowWithIntl( );
+ */
+export const shallowWithIntl = (children: React.ReactNode) => {
+ const context = { context: { intl } };
+
+ return shallow({children} , context)
+ .childAt(0)
+ .dive(context)
+ .shallow();
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/assets/engine.svg b/x-pack/plugins/enterprise_search/public/applications/app_search/assets/engine.svg
new file mode 100644
index 0000000000000..ceab918e92e70
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/assets/engine.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/assets/getting_started.png b/x-pack/plugins/enterprise_search/public/applications/app_search/assets/getting_started.png
new file mode 100644
index 0000000000000..4d988d14f0483
Binary files /dev/null and b/x-pack/plugins/enterprise_search/public/applications/app_search/assets/getting_started.png differ
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/assets/logo.svg b/x-pack/plugins/enterprise_search/public/applications/app_search/assets/logo.svg
new file mode 100644
index 0000000000000..2284a425b5add
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/assets/logo.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/assets/meta_engine.svg b/x-pack/plugins/enterprise_search/public/applications/app_search/assets/meta_engine.svg
new file mode 100644
index 0000000000000..4e01e9a0b34fb
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/assets/meta_engine.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_state.tsx
new file mode 100644
index 0000000000000..9bb5cd3bffdf5
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_state.tsx
@@ -0,0 +1,74 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useContext } from 'react';
+import { EuiPage, EuiPageBody, EuiPageContent, EuiEmptyPrompt, EuiButton } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import { sendTelemetry } from '../../../shared/telemetry';
+import { SetAppSearchBreadcrumbs as SetBreadcrumbs } from '../../../shared/kibana_breadcrumbs';
+import { KibanaContext, IKibanaContext } from '../../../index';
+
+import { EngineOverviewHeader } from '../engine_overview_header';
+
+import './empty_states.scss';
+
+export const EmptyState: React.FC = () => {
+ const { enterpriseSearchUrl, http } = useContext(KibanaContext) as IKibanaContext;
+
+ const buttonProps = {
+ href: `${enterpriseSearchUrl}/as/engines/new`,
+ target: '_blank',
+ onClick: () =>
+ sendTelemetry({
+ http,
+ product: 'app_search',
+ action: 'clicked',
+ metric: 'create_first_engine_button',
+ }),
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+ }
+ titleSize="l"
+ body={
+
+
+
+ }
+ actions={
+
+
+
+ }
+ />
+
+
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.scss
new file mode 100644
index 0000000000000..01b0903add559
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.scss
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+/**
+ * Empty/Error UI states
+ */
+.emptyState {
+ min-height: $euiSizeXXL * 11.25;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+
+ &__prompt > .euiIcon {
+ margin-bottom: $euiSizeS;
+ }
+}
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.test.tsx
new file mode 100644
index 0000000000000..12bf003564103
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/empty_states.test.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import '../../../__mocks__/shallow_usecontext.mock';
+
+import React from 'react';
+import { shallow } from 'enzyme';
+import { EuiEmptyPrompt, EuiButton, EuiLoadingContent } from '@elastic/eui';
+
+jest.mock('../../../shared/telemetry', () => ({
+ sendTelemetry: jest.fn(),
+ SendAppSearchTelemetry: jest.fn(),
+}));
+import { sendTelemetry } from '../../../shared/telemetry';
+
+import { ErrorState, EmptyState, LoadingState } from './';
+
+describe('ErrorState', () => {
+ it('renders', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1);
+ });
+});
+
+describe('EmptyState', () => {
+ it('renders', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1);
+ });
+
+ it('sends telemetry on create first engine click', () => {
+ const wrapper = shallow( );
+ const prompt = wrapper.find(EuiEmptyPrompt).dive();
+ const button = prompt.find(EuiButton);
+
+ button.simulate('click');
+ expect(sendTelemetry).toHaveBeenCalled();
+ (sendTelemetry as jest.Mock).mockClear();
+ });
+});
+
+describe('LoadingState', () => {
+ it('renders', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find(EuiLoadingContent)).toHaveLength(2);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/error_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/error_state.tsx
new file mode 100644
index 0000000000000..d8eeff2aba1c6
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/error_state.tsx
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useContext } from 'react';
+import { EuiPage, EuiPageBody, EuiPageContent, EuiEmptyPrompt, EuiCode } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import { EuiButton } from '../../../shared/react_router_helpers';
+import { SetAppSearchBreadcrumbs as SetBreadcrumbs } from '../../../shared/kibana_breadcrumbs';
+import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry';
+import { KibanaContext, IKibanaContext } from '../../../index';
+import { EngineOverviewHeader } from '../engine_overview_header';
+
+import './empty_states.scss';
+
+export const ErrorState: React.FC = () => {
+ const { enterpriseSearchUrl } = useContext(KibanaContext) as IKibanaContext;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ }
+ titleSize="l"
+ body={
+ <>
+
+ {enterpriseSearchUrl},
+ }}
+ />
+
+
+
+ config/kibana.yml,
+ }}
+ />
+
+
+
+
+
+ [enterpriseSearch][plugins],
+ }}
+ />
+
+
+ >
+ }
+ actions={
+
+
+
+ }
+ />
+
+
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/index.ts
new file mode 100644
index 0000000000000..e92bf214c4cc7
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { LoadingState } from './loading_state';
+export { EmptyState } from './empty_state';
+export { ErrorState } from './error_state';
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/loading_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/loading_state.tsx
new file mode 100644
index 0000000000000..2be917c8df096
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/empty_states/loading_state.tsx
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { EuiPage, EuiPageBody, EuiPageContent, EuiSpacer, EuiLoadingContent } from '@elastic/eui';
+
+import { SetAppSearchBreadcrumbs as SetBreadcrumbs } from '../../../shared/kibana_breadcrumbs';
+import { EngineOverviewHeader } from '../engine_overview_header';
+
+import './empty_states.scss';
+
+export const LoadingState: React.FC = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.scss
new file mode 100644
index 0000000000000..2c7f7de6458e2
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.scss
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+/**
+ * Engine Overview
+ */
+.engineOverview {
+ width: 100%;
+
+ &__body {
+ padding: $euiSize;
+
+ @include euiBreakpoint('m', 'l', 'xl') {
+ padding: $euiSizeXL;
+ }
+ }
+}
+
+.engineIcon {
+ display: inline-block;
+ width: $euiSize;
+ height: $euiSize;
+ margin-right: $euiSizeXS;
+}
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx
new file mode 100644
index 0000000000000..4d2a2ea1df9aa
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.test.tsx
@@ -0,0 +1,171 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import '../../../__mocks__/react_router_history.mock';
+
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import { render, ReactWrapper } from 'enzyme';
+
+import { I18nProvider } from '@kbn/i18n/react';
+import { KibanaContext } from '../../../';
+import { LicenseContext } from '../../../shared/licensing';
+import { mountWithContext, mockKibanaContext } from '../../../__mocks__';
+
+import { EmptyState, ErrorState } from '../empty_states';
+import { EngineTable, IEngineTablePagination } from './engine_table';
+
+import { EngineOverview } from './';
+
+describe('EngineOverview', () => {
+ describe('non-happy-path states', () => {
+ it('isLoading', () => {
+ // We use render() instead of mount() here to not trigger lifecycle methods (i.e., useEffect)
+ // TODO: Consider pulling this out to a renderWithContext mock/helper
+ const wrapper: Cheerio = render(
+
+
+
+
+
+
+
+ );
+
+ // render() directly renders HTML which means we have to look for selectors instead of for LoadingState directly
+ expect(wrapper.find('.euiLoadingContent')).toHaveLength(2);
+ });
+
+ it('isEmpty', async () => {
+ const wrapper = await mountWithApiMock({
+ get: () => ({
+ results: [],
+ meta: { page: { total_results: 0 } },
+ }),
+ });
+
+ expect(wrapper.find(EmptyState)).toHaveLength(1);
+ });
+
+ it('hasErrorConnecting', async () => {
+ const wrapper = await mountWithApiMock({
+ get: () => ({ invalidPayload: true }),
+ });
+ expect(wrapper.find(ErrorState)).toHaveLength(1);
+ });
+ });
+
+ describe('happy-path states', () => {
+ const mockedApiResponse = {
+ results: [
+ {
+ name: 'hello-world',
+ created_at: 'Fri, 1 Jan 1970 12:00:00 +0000',
+ document_count: 50,
+ field_count: 10,
+ },
+ ],
+ meta: {
+ page: {
+ current: 1,
+ total_pages: 10,
+ total_results: 100,
+ size: 10,
+ },
+ },
+ };
+ const mockApi = jest.fn(() => mockedApiResponse);
+ let wrapper: ReactWrapper;
+
+ beforeAll(async () => {
+ wrapper = await mountWithApiMock({ get: mockApi });
+ });
+
+ it('renders', () => {
+ expect(wrapper.find(EngineTable)).toHaveLength(1);
+ });
+
+ it('calls the engines API', () => {
+ expect(mockApi).toHaveBeenNthCalledWith(1, '/api/app_search/engines', {
+ query: {
+ type: 'indexed',
+ pageIndex: 1,
+ },
+ });
+ });
+
+ describe('pagination', () => {
+ const getTablePagination: () => IEngineTablePagination = () =>
+ wrapper.find(EngineTable).first().prop('pagination');
+
+ it('passes down page data from the API', () => {
+ const pagination = getTablePagination();
+
+ expect(pagination.totalEngines).toEqual(100);
+ expect(pagination.pageIndex).toEqual(0);
+ });
+
+ it('re-polls the API on page change', async () => {
+ await act(async () => getTablePagination().onPaginate(5));
+ wrapper.update();
+
+ expect(mockApi).toHaveBeenLastCalledWith('/api/app_search/engines', {
+ query: {
+ type: 'indexed',
+ pageIndex: 5,
+ },
+ });
+ expect(getTablePagination().pageIndex).toEqual(4);
+ });
+ });
+
+ describe('when on a platinum license', () => {
+ beforeAll(async () => {
+ mockApi.mockClear();
+ wrapper = await mountWithApiMock({
+ license: { type: 'platinum', isActive: true },
+ get: mockApi,
+ });
+ });
+
+ it('renders a 2nd meta engines table', () => {
+ expect(wrapper.find(EngineTable)).toHaveLength(2);
+ });
+
+ it('makes a 2nd call to the engines API with type meta', () => {
+ expect(mockApi).toHaveBeenNthCalledWith(2, '/api/app_search/engines', {
+ query: {
+ type: 'meta',
+ pageIndex: 1,
+ },
+ });
+ });
+ });
+ });
+
+ /**
+ * Test helpers
+ */
+
+ const mountWithApiMock = async ({ get, license }: { get(): any; license?: object }) => {
+ let wrapper: ReactWrapper | undefined;
+ const httpMock = { ...mockKibanaContext.http, get };
+
+ // We get a lot of act() warning/errors in the terminal without this.
+ // TBH, I don't fully understand why since Enzyme's mount is supposed to
+ // have act() baked in - could be because of the wrapping context provider?
+ await act(async () => {
+ wrapper = mountWithContext( , { http: httpMock, license });
+ });
+ if (wrapper) {
+ wrapper.update(); // This seems to be required for the DOM to actually update
+
+ return wrapper;
+ } else {
+ throw new Error('Could not mount wrapper');
+ }
+ };
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx
new file mode 100644
index 0000000000000..13d092a657d11
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx
@@ -0,0 +1,155 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useContext, useEffect, useState } from 'react';
+import {
+ EuiPage,
+ EuiPageBody,
+ EuiPageContent,
+ EuiPageContentHeader,
+ EuiPageContentBody,
+ EuiTitle,
+ EuiSpacer,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import { SetAppSearchBreadcrumbs as SetBreadcrumbs } from '../../../shared/kibana_breadcrumbs';
+import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry';
+import { LicenseContext, ILicenseContext, hasPlatinumLicense } from '../../../shared/licensing';
+import { KibanaContext, IKibanaContext } from '../../../index';
+
+import EnginesIcon from '../../assets/engine.svg';
+import MetaEnginesIcon from '../../assets/meta_engine.svg';
+
+import { LoadingState, EmptyState, ErrorState } from '../empty_states';
+import { EngineOverviewHeader } from '../engine_overview_header';
+import { EngineTable } from './engine_table';
+
+import './engine_overview.scss';
+
+interface IGetEnginesParams {
+ type: string;
+ pageIndex: number;
+}
+interface ISetEnginesCallbacks {
+ setResults: React.Dispatch>;
+ setResultsTotal: React.Dispatch>;
+}
+
+export const EngineOverview: React.FC = () => {
+ const { http } = useContext(KibanaContext) as IKibanaContext;
+ const { license } = useContext(LicenseContext) as ILicenseContext;
+
+ const [isLoading, setIsLoading] = useState(true);
+ const [hasErrorConnecting, setHasErrorConnecting] = useState(false);
+
+ const [engines, setEngines] = useState([]);
+ const [enginesPage, setEnginesPage] = useState(1);
+ const [enginesTotal, setEnginesTotal] = useState(0);
+ const [metaEngines, setMetaEngines] = useState([]);
+ const [metaEnginesPage, setMetaEnginesPage] = useState(1);
+ const [metaEnginesTotal, setMetaEnginesTotal] = useState(0);
+
+ const getEnginesData = async ({ type, pageIndex }: IGetEnginesParams) => {
+ return await http.get('/api/app_search/engines', {
+ query: { type, pageIndex },
+ });
+ };
+ const setEnginesData = async (params: IGetEnginesParams, callbacks: ISetEnginesCallbacks) => {
+ try {
+ const response = await getEnginesData(params);
+
+ callbacks.setResults(response.results);
+ callbacks.setResultsTotal(response.meta.page.total_results);
+
+ setIsLoading(false);
+ } catch (error) {
+ setHasErrorConnecting(true);
+ }
+ };
+
+ useEffect(() => {
+ const params = { type: 'indexed', pageIndex: enginesPage };
+ const callbacks = { setResults: setEngines, setResultsTotal: setEnginesTotal };
+
+ setEnginesData(params, callbacks);
+ }, [enginesPage]);
+
+ useEffect(() => {
+ if (hasPlatinumLicense(license)) {
+ const params = { type: 'meta', pageIndex: metaEnginesPage };
+ const callbacks = { setResults: setMetaEngines, setResultsTotal: setMetaEnginesTotal };
+
+ setEnginesData(params, callbacks);
+ }
+ }, [license, metaEnginesPage]);
+
+ if (hasErrorConnecting) return ;
+ if (isLoading) return ;
+ if (!engines.length) return ;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {metaEngines.length > 0 && (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+
+
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx
new file mode 100644
index 0000000000000..46b6e61e352de
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.test.tsx
@@ -0,0 +1,80 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { EuiBasicTable, EuiPagination, EuiButtonEmpty, EuiLink } from '@elastic/eui';
+
+import { mountWithContext } from '../../../__mocks__';
+jest.mock('../../../shared/telemetry', () => ({ sendTelemetry: jest.fn() }));
+import { sendTelemetry } from '../../../shared/telemetry';
+
+import { EngineTable } from './engine_table';
+
+describe('EngineTable', () => {
+ const onPaginate = jest.fn(); // onPaginate updates the engines API call upstream
+
+ const wrapper = mountWithContext(
+
+ );
+ const table = wrapper.find(EuiBasicTable);
+
+ it('renders', () => {
+ expect(table).toHaveLength(1);
+ expect(table.prop('pagination').totalItemCount).toEqual(50);
+
+ const tableContent = table.text();
+ expect(tableContent).toContain('test-engine');
+ expect(tableContent).toContain('January 1, 1970');
+ expect(tableContent).toContain('99,999');
+ expect(tableContent).toContain('10');
+
+ expect(table.find(EuiPagination).find(EuiButtonEmpty)).toHaveLength(5); // Should display 5 pages at 10 engines per page
+ });
+
+ it('contains engine links which send telemetry', () => {
+ const engineLinks = wrapper.find(EuiLink);
+
+ engineLinks.forEach((link) => {
+ expect(link.prop('href')).toEqual('http://localhost:3002/as/engines/test-engine');
+ link.simulate('click');
+
+ expect(sendTelemetry).toHaveBeenCalledWith({
+ http: expect.any(Object),
+ product: 'app_search',
+ action: 'clicked',
+ metric: 'engine_table_link',
+ });
+ });
+ });
+
+ it('triggers onPaginate', () => {
+ table.prop('onChange')({ page: { index: 4 } });
+
+ expect(onPaginate).toHaveBeenCalledWith(5);
+ });
+
+ it('handles empty data', () => {
+ const emptyWrapper = mountWithContext(
+ {} }} />
+ );
+ const emptyTable = emptyWrapper.find(EuiBasicTable);
+ expect(emptyTable.prop('pagination').pageIndex).toEqual(0);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx
new file mode 100644
index 0000000000000..1e58d820dc83b
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_table.tsx
@@ -0,0 +1,153 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useContext } from 'react';
+import { EuiBasicTable, EuiBasicTableColumn, EuiLink } from '@elastic/eui';
+import { FormattedMessage, FormattedDate, FormattedNumber } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+
+import { sendTelemetry } from '../../../shared/telemetry';
+import { KibanaContext, IKibanaContext } from '../../../index';
+
+import { ENGINES_PAGE_SIZE } from '../../../../../common/constants';
+
+export interface IEngineTableData {
+ name: string;
+ created_at: string;
+ document_count: number;
+ field_count: number;
+}
+export interface IEngineTablePagination {
+ totalEngines: number;
+ pageIndex: number;
+ onPaginate(pageIndex: number): void;
+}
+export interface IEngineTableProps {
+ data: IEngineTableData[];
+ pagination: IEngineTablePagination;
+}
+export interface IOnChange {
+ page: {
+ index: number;
+ };
+}
+
+export const EngineTable: React.FC = ({
+ data,
+ pagination: { totalEngines, pageIndex, onPaginate },
+}) => {
+ const { enterpriseSearchUrl, http } = useContext(KibanaContext) as IKibanaContext;
+ const engineLinkProps = (name: string) => ({
+ href: `${enterpriseSearchUrl}/as/engines/${name}`,
+ target: '_blank',
+ onClick: () =>
+ sendTelemetry({
+ http,
+ product: 'app_search',
+ action: 'clicked',
+ metric: 'engine_table_link',
+ }),
+ });
+
+ const columns: Array> = [
+ {
+ field: 'name',
+ name: i18n.translate('xpack.enterpriseSearch.appSearch.enginesOverview.table.column.name', {
+ defaultMessage: 'Name',
+ }),
+ render: (name: string) => (
+
+ {name}
+
+ ),
+ width: '30%',
+ truncateText: true,
+ mobileOptions: {
+ header: true,
+ // Note: the below props are valid props per https://elastic.github.io/eui/#/tabular-content/tables (Responsive tables), but EUI's types have a bug reporting it as an error
+ // @ts-ignore
+ enlarge: true,
+ fullWidth: true,
+ truncateText: false,
+ },
+ },
+ {
+ field: 'created_at',
+ name: i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.enginesOverview.table.column.createdAt',
+ {
+ defaultMessage: 'Created At',
+ }
+ ),
+ dataType: 'string',
+ render: (dateString: string) => (
+ // e.g., January 1, 1970
+
+ ),
+ },
+ {
+ field: 'document_count',
+ name: i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.enginesOverview.table.column.documentCount',
+ {
+ defaultMessage: 'Document Count',
+ }
+ ),
+ dataType: 'number',
+ render: (number: number) => ,
+ truncateText: true,
+ },
+ {
+ field: 'field_count',
+ name: i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.enginesOverview.table.column.fieldCount',
+ {
+ defaultMessage: 'Field Count',
+ }
+ ),
+ dataType: 'number',
+ render: (number: number) => ,
+ truncateText: true,
+ },
+ {
+ field: 'name',
+ name: i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.enginesOverview.table.column.actions',
+ {
+ defaultMessage: 'Actions',
+ }
+ ),
+ dataType: 'string',
+ render: (name: string) => (
+
+
+
+ ),
+ align: 'right',
+ width: '100px',
+ },
+ ];
+
+ return (
+ {
+ const { index } = page;
+ onPaginate(index + 1); // Note on paging - App Search's API pages start at 1, EuiBasicTables' pages start at 0
+ }}
+ />
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/index.ts
new file mode 100644
index 0000000000000..48b7645dc39e8
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { EngineOverview } from './engine_overview';
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.test.tsx
new file mode 100644
index 0000000000000..2e49540270ef0
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.test.tsx
@@ -0,0 +1,41 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import '../../../__mocks__/shallow_usecontext.mock';
+
+import React from 'react';
+import { shallow } from 'enzyme';
+
+jest.mock('../../../shared/telemetry', () => ({ sendTelemetry: jest.fn() }));
+import { sendTelemetry } from '../../../shared/telemetry';
+
+import { EngineOverviewHeader } from '../engine_overview_header';
+
+describe('EngineOverviewHeader', () => {
+ it('renders', () => {
+ const wrapper = shallow( );
+ expect(wrapper.find('h1')).toHaveLength(1);
+ });
+
+ it('renders a launch app search button that sends telemetry on click', () => {
+ const wrapper = shallow( );
+ const button = wrapper.find('[data-test-subj="launchButton"]');
+
+ expect(button.prop('href')).toBe('http://localhost:3002/as');
+ expect(button.prop('isDisabled')).toBeFalsy();
+
+ button.simulate('click');
+ expect(sendTelemetry).toHaveBeenCalled();
+ });
+
+ it('renders a disabled button when isButtonDisabled is true', () => {
+ const wrapper = shallow( );
+ const button = wrapper.find('[data-test-subj="launchButton"]');
+
+ expect(button.prop('isDisabled')).toBe(true);
+ expect(button.prop('href')).toBeUndefined();
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.tsx
new file mode 100644
index 0000000000000..9aafa8ec0380c
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/engine_overview_header.tsx
@@ -0,0 +1,72 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useContext } from 'react';
+import {
+ EuiPageHeader,
+ EuiPageHeaderSection,
+ EuiTitle,
+ EuiButton,
+ EuiButtonProps,
+ EuiLinkProps,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+
+import { sendTelemetry } from '../../../shared/telemetry';
+import { KibanaContext, IKibanaContext } from '../../../index';
+
+interface IEngineOverviewHeaderProps {
+ isButtonDisabled?: boolean;
+}
+
+export const EngineOverviewHeader: React.FC = ({
+ isButtonDisabled,
+}) => {
+ const { enterpriseSearchUrl, http } = useContext(KibanaContext) as IKibanaContext;
+
+ const buttonProps = {
+ fill: true,
+ iconType: 'popout',
+ 'data-test-subj': 'launchButton',
+ } as EuiButtonProps & EuiLinkProps;
+
+ if (isButtonDisabled) {
+ buttonProps.isDisabled = true;
+ } else {
+ buttonProps.href = `${enterpriseSearchUrl}/as`;
+ buttonProps.target = '_blank';
+ buttonProps.onClick = () =>
+ sendTelemetry({
+ http,
+ product: 'app_search',
+ action: 'clicked',
+ metric: 'header_launch_button',
+ });
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/index.ts
new file mode 100644
index 0000000000000..2d37f037e21e5
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview_header/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { EngineOverviewHeader } from './engine_overview_header';
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/index.ts
new file mode 100644
index 0000000000000..c367424d375f9
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { SetupGuide } from './setup_guide';
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.test.tsx
new file mode 100644
index 0000000000000..82cc344d49632
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.test.tsx
@@ -0,0 +1,21 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { SetAppSearchBreadcrumbs as SetBreadcrumbs } from '../../../shared/kibana_breadcrumbs';
+import { SetupGuide as SetupGuideLayout } from '../../../shared/setup_guide';
+import { SetupGuide } from './';
+
+describe('SetupGuide', () => {
+ it('renders', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find(SetupGuideLayout)).toHaveLength(1);
+ expect(wrapper.find(SetBreadcrumbs)).toHaveLength(1);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx
new file mode 100644
index 0000000000000..df278bf938a69
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx
@@ -0,0 +1,64 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { EuiSpacer, EuiTitle, EuiText } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+
+import { SetupGuide as SetupGuideLayout } from '../../../shared/setup_guide';
+import { SetAppSearchBreadcrumbs as SetBreadcrumbs } from '../../../shared/kibana_breadcrumbs';
+import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry';
+import GettingStarted from '../../assets/getting_started.png';
+
+export const SetupGuide: React.FC = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx
new file mode 100644
index 0000000000000..45e318ca0f9d9
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx
@@ -0,0 +1,46 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import '../__mocks__/shallow_usecontext.mock';
+
+import React, { useContext } from 'react';
+import { Redirect } from 'react-router-dom';
+import { shallow } from 'enzyme';
+
+import { SetupGuide } from './components/setup_guide';
+import { EngineOverview } from './components/engine_overview';
+
+import { AppSearch } from './';
+
+describe('App Search Routes', () => {
+ describe('/', () => {
+ it('redirects to Setup Guide when enterpriseSearchUrl is not set', () => {
+ (useContext as jest.Mock).mockImplementationOnce(() => ({ enterpriseSearchUrl: '' }));
+ const wrapper = shallow( );
+
+ expect(wrapper.find(Redirect)).toHaveLength(1);
+ expect(wrapper.find(EngineOverview)).toHaveLength(0);
+ });
+
+ it('renders Engine Overview when enterpriseSearchUrl is set', () => {
+ (useContext as jest.Mock).mockImplementationOnce(() => ({
+ enterpriseSearchUrl: 'https://foo.bar',
+ }));
+ const wrapper = shallow( );
+
+ expect(wrapper.find(EngineOverview)).toHaveLength(1);
+ expect(wrapper.find(Redirect)).toHaveLength(0);
+ });
+ });
+
+ describe('/setup_guide', () => {
+ it('renders', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find(SetupGuide)).toHaveLength(1);
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx
new file mode 100644
index 0000000000000..8f7142f1631a9
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useContext } from 'react';
+import { Route, Redirect } from 'react-router-dom';
+
+import { KibanaContext, IKibanaContext } from '../index';
+
+import { SetupGuide } from './components/setup_guide';
+import { EngineOverview } from './components/engine_overview';
+
+export const AppSearch: React.FC = () => {
+ const { enterpriseSearchUrl } = useContext(KibanaContext) as IKibanaContext;
+
+ return (
+ <>
+
+ {!enterpriseSearchUrl ? : }
+
+
+
+
+ >
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx
new file mode 100644
index 0000000000000..1aead8468ca3b
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx
@@ -0,0 +1,40 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+
+import { coreMock } from 'src/core/public/mocks';
+import { licensingMock } from '../../../licensing/public/mocks';
+
+import { renderApp } from './';
+import { AppSearch } from './app_search';
+
+describe('renderApp', () => {
+ const params = coreMock.createAppMountParamters();
+ const core = coreMock.createStart();
+ const config = {};
+ const plugins = {
+ licensing: licensingMock.createSetup(),
+ } as any;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('mounts and unmounts UI', () => {
+ const MockApp = () => Hello world!
;
+
+ const unmount = renderApp(MockApp, core, params, config, plugins);
+ expect(params.element.querySelector('.hello-world')).not.toBeNull();
+ unmount();
+ expect(params.element.innerHTML).toEqual('');
+ });
+
+ it('renders AppSearch', () => {
+ renderApp(AppSearch, core, params, config, plugins);
+ expect(params.element.querySelector('.setupGuide')).not.toBeNull();
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx
new file mode 100644
index 0000000000000..4ef7aca8260a2
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/index.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { Router } from 'react-router-dom';
+
+import { I18nProvider } from '@kbn/i18n/react';
+import { CoreStart, AppMountParameters, HttpSetup, ChromeBreadcrumb } from 'src/core/public';
+import { ClientConfigType, PluginsSetup } from '../plugin';
+import { LicenseProvider } from './shared/licensing';
+
+export interface IKibanaContext {
+ enterpriseSearchUrl?: string;
+ http: HttpSetup;
+ setBreadcrumbs(crumbs: ChromeBreadcrumb[]): void;
+}
+
+export const KibanaContext = React.createContext({});
+
+/**
+ * This file serves as a reusable wrapper to share Kibana-level context and other helpers
+ * between various Enterprise Search plugins (e.g. AppSearch, WorkplaceSearch, ES landing page)
+ * which should be imported and passed in as the first param in plugin.ts.
+ */
+
+export const renderApp = (
+ App: React.FC,
+ core: CoreStart,
+ params: AppMountParameters,
+ config: ClientConfigType,
+ plugins: PluginsSetup
+) => {
+ ReactDOM.render(
+
+
+
+
+
+
+
+
+ ,
+ params.element
+ );
+ return () => ReactDOM.unmountComponentAtNode(params.element);
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.test.ts
new file mode 100644
index 0000000000000..42f308c554268
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.test.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { getPublicUrl } from './';
+
+describe('Enterprise Search URL helper', () => {
+ const httpMock = { get: jest.fn() } as any;
+
+ it('calls and returns the public URL API endpoint', async () => {
+ httpMock.get.mockImplementationOnce(() => ({ publicUrl: 'http://some.vanity.url' }));
+
+ expect(await getPublicUrl(httpMock)).toEqual('http://some.vanity.url');
+ });
+
+ it('strips trailing slashes', async () => {
+ httpMock.get.mockImplementationOnce(() => ({ publicUrl: 'http://trailing.slash/' }));
+
+ expect(await getPublicUrl(httpMock)).toEqual('http://trailing.slash');
+ });
+
+ // For the most part, error logging/handling is done on the server side.
+ // On the front-end, we should simply gracefully fall back to config.host
+ // if we can't fetch a public URL
+ it('falls back to an empty string', async () => {
+ expect(await getPublicUrl(httpMock)).toEqual('');
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.ts b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.ts
new file mode 100644
index 0000000000000..419c187a0048a
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/get_enterprise_search_url.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { HttpSetup } from 'src/core/public';
+
+/**
+ * On Elastic Cloud, the host URL set in kibana.yml is not necessarily the same
+ * URL we want to send users to in the front-end (e.g. if a vanity URL is set).
+ *
+ * This helper checks a Kibana API endpoint (which has checks an Enterprise
+ * Search internal API endpoint) for the correct public-facing URL to use.
+ */
+export const getPublicUrl = async (http: HttpSetup): Promise => {
+ try {
+ const { publicUrl } = await http.get('/api/enterprise_search/public_url');
+ return stripTrailingSlash(publicUrl);
+ } catch {
+ return '';
+ }
+};
+
+const stripTrailingSlash = (url: string): string => {
+ return url.endsWith('/') ? url.slice(0, -1) : url;
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/index.ts
new file mode 100644
index 0000000000000..bbbb688b8ea7b
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/enterprise_search_url/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { getPublicUrl } from './get_enterprise_search_url';
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.test.ts
new file mode 100644
index 0000000000000..7ea73577c4de6
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.test.ts
@@ -0,0 +1,206 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { generateBreadcrumb } from './generate_breadcrumbs';
+import { appSearchBreadcrumbs, enterpriseSearchBreadcrumbs } from './';
+
+import { mockHistory as mockHistoryUntyped } from '../../__mocks__';
+const mockHistory = mockHistoryUntyped as any;
+
+jest.mock('../react_router_helpers', () => ({ letBrowserHandleEvent: jest.fn(() => false) }));
+import { letBrowserHandleEvent } from '../react_router_helpers';
+
+describe('generateBreadcrumb', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it("creates a breadcrumb object matching EUI's breadcrumb type", () => {
+ const breadcrumb = generateBreadcrumb({
+ text: 'Hello World',
+ path: '/hello_world',
+ history: mockHistory,
+ });
+ expect(breadcrumb).toEqual({
+ text: 'Hello World',
+ href: '/enterprise_search/hello_world',
+ onClick: expect.any(Function),
+ });
+ });
+
+ it('prevents default navigation and uses React Router history on click', () => {
+ const breadcrumb = generateBreadcrumb({ text: '', path: '/', history: mockHistory }) as any;
+ const event = { preventDefault: jest.fn() };
+ breadcrumb.onClick(event);
+
+ expect(mockHistory.push).toHaveBeenCalled();
+ expect(event.preventDefault).toHaveBeenCalled();
+ });
+
+ it('does not prevent default browser behavior on new tab/window clicks', () => {
+ const breadcrumb = generateBreadcrumb({ text: '', path: '/', history: mockHistory }) as any;
+
+ (letBrowserHandleEvent as jest.Mock).mockImplementationOnce(() => true);
+ breadcrumb.onClick();
+
+ expect(mockHistory.push).not.toHaveBeenCalled();
+ });
+
+ it('does not generate link behavior if path is excluded', () => {
+ const breadcrumb = generateBreadcrumb({ text: 'Unclickable breadcrumb' });
+
+ expect(breadcrumb.href).toBeUndefined();
+ expect(breadcrumb.onClick).toBeUndefined();
+ });
+});
+
+describe('enterpriseSearchBreadcrumbs', () => {
+ const breadCrumbs = [
+ {
+ text: 'Page 1',
+ path: '/page1',
+ },
+ {
+ text: 'Page 2',
+ path: '/page2',
+ },
+ ];
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const subject = () => enterpriseSearchBreadcrumbs(mockHistory)(breadCrumbs);
+
+ it('Builds a chain of breadcrumbs with Enterprise Search at the root', () => {
+ expect(subject()).toEqual([
+ {
+ text: 'Enterprise Search',
+ },
+ {
+ href: '/enterprise_search/page1',
+ onClick: expect.any(Function),
+ text: 'Page 1',
+ },
+ {
+ href: '/enterprise_search/page2',
+ onClick: expect.any(Function),
+ text: 'Page 2',
+ },
+ ]);
+ });
+
+ it('shows just the root if breadcrumbs is empty', () => {
+ expect(enterpriseSearchBreadcrumbs(mockHistory)()).toEqual([
+ {
+ text: 'Enterprise Search',
+ },
+ ]);
+ });
+
+ describe('links', () => {
+ const eventMock = {
+ preventDefault: jest.fn(),
+ } as any;
+
+ it('has Enterprise Search text first', () => {
+ expect(subject()[0].onClick).toBeUndefined();
+ });
+
+ it('has a link to page 1 second', () => {
+ (subject()[1] as any).onClick(eventMock);
+ expect(mockHistory.push).toHaveBeenCalledWith('/page1');
+ });
+
+ it('has a link to page 2 last', () => {
+ (subject()[2] as any).onClick(eventMock);
+ expect(mockHistory.push).toHaveBeenCalledWith('/page2');
+ });
+ });
+});
+
+describe('appSearchBreadcrumbs', () => {
+ const breadCrumbs = [
+ {
+ text: 'Page 1',
+ path: '/page1',
+ },
+ {
+ text: 'Page 2',
+ path: '/page2',
+ },
+ ];
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockHistory.createHref.mockImplementation(
+ ({ pathname }: any) => `/enterprise_search/app_search${pathname}`
+ );
+ });
+
+ const subject = () => appSearchBreadcrumbs(mockHistory)(breadCrumbs);
+
+ it('Builds a chain of breadcrumbs with Enterprise Search and App Search at the root', () => {
+ expect(subject()).toEqual([
+ {
+ text: 'Enterprise Search',
+ },
+ {
+ href: '/enterprise_search/app_search/',
+ onClick: expect.any(Function),
+ text: 'App Search',
+ },
+ {
+ href: '/enterprise_search/app_search/page1',
+ onClick: expect.any(Function),
+ text: 'Page 1',
+ },
+ {
+ href: '/enterprise_search/app_search/page2',
+ onClick: expect.any(Function),
+ text: 'Page 2',
+ },
+ ]);
+ });
+
+ it('shows just the root if breadcrumbs is empty', () => {
+ expect(appSearchBreadcrumbs(mockHistory)()).toEqual([
+ {
+ text: 'Enterprise Search',
+ },
+ {
+ href: '/enterprise_search/app_search/',
+ onClick: expect.any(Function),
+ text: 'App Search',
+ },
+ ]);
+ });
+
+ describe('links', () => {
+ const eventMock = {
+ preventDefault: jest.fn(),
+ } as any;
+
+ it('has Enterprise Search text first', () => {
+ expect(subject()[0].onClick).toBeUndefined();
+ });
+
+ it('has a link to App Search second', () => {
+ (subject()[1] as any).onClick(eventMock);
+ expect(mockHistory.push).toHaveBeenCalledWith('/');
+ });
+
+ it('has a link to page 1 third', () => {
+ (subject()[2] as any).onClick(eventMock);
+ expect(mockHistory.push).toHaveBeenCalledWith('/page1');
+ });
+
+ it('has a link to page 2 last', () => {
+ (subject()[3] as any).onClick(eventMock);
+ expect(mockHistory.push).toHaveBeenCalledWith('/page2');
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.ts
new file mode 100644
index 0000000000000..8f72875a32bd4
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/generate_breadcrumbs.ts
@@ -0,0 +1,54 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { EuiBreadcrumb } from '@elastic/eui';
+import { History } from 'history';
+
+import { letBrowserHandleEvent } from '../react_router_helpers';
+
+/**
+ * Generate React-Router-friendly EUI breadcrumb objects
+ * https://elastic.github.io/eui/#/navigation/breadcrumbs
+ */
+
+interface IGenerateBreadcrumbProps {
+ text: string;
+ path?: string;
+ history?: History;
+}
+
+export const generateBreadcrumb = ({ text, path, history }: IGenerateBreadcrumbProps) => {
+ const breadcrumb = { text } as EuiBreadcrumb;
+
+ if (path && history) {
+ breadcrumb.href = history.createHref({ pathname: path });
+ breadcrumb.onClick = (event) => {
+ if (letBrowserHandleEvent(event)) return;
+ event.preventDefault();
+ history.push(path);
+ };
+ }
+
+ return breadcrumb;
+};
+
+/**
+ * Product-specific breadcrumb helpers
+ */
+
+export type TBreadcrumbs = IGenerateBreadcrumbProps[];
+
+export const enterpriseSearchBreadcrumbs = (history: History) => (
+ breadcrumbs: TBreadcrumbs = []
+) => [
+ generateBreadcrumb({ text: 'Enterprise Search' }),
+ ...breadcrumbs.map(({ text, path }: IGenerateBreadcrumbProps) =>
+ generateBreadcrumb({ text, path, history })
+ ),
+];
+
+export const appSearchBreadcrumbs = (history: History) => (breadcrumbs: TBreadcrumbs = []) =>
+ enterpriseSearchBreadcrumbs(history)([{ text: 'App Search', path: '/' }, ...breadcrumbs]);
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/index.ts
new file mode 100644
index 0000000000000..cf8bbbc593f2f
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { enterpriseSearchBreadcrumbs } from './generate_breadcrumbs';
+export { appSearchBreadcrumbs } from './generate_breadcrumbs';
+export { SetAppSearchBreadcrumbs } from './set_breadcrumbs';
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.test.tsx
new file mode 100644
index 0000000000000..974ca54277c51
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.test.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+
+import '../../__mocks__/react_router_history.mock';
+import { mountWithKibanaContext } from '../../__mocks__';
+
+jest.mock('./generate_breadcrumbs', () => ({ appSearchBreadcrumbs: jest.fn() }));
+import { appSearchBreadcrumbs, SetAppSearchBreadcrumbs } from './';
+
+describe('SetAppSearchBreadcrumbs', () => {
+ const setBreadcrumbs = jest.fn();
+ const builtBreadcrumbs = [] as any;
+ const appSearchBreadCrumbsInnerCall = jest.fn().mockReturnValue(builtBreadcrumbs);
+ const appSearchBreadCrumbsOuterCall = jest.fn().mockReturnValue(appSearchBreadCrumbsInnerCall);
+ (appSearchBreadcrumbs as jest.Mock).mockImplementation(appSearchBreadCrumbsOuterCall);
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ const mountSetAppSearchBreadcrumbs = (props: any) => {
+ return mountWithKibanaContext( , {
+ http: {},
+ enterpriseSearchUrl: 'http://localhost:3002',
+ setBreadcrumbs,
+ });
+ };
+
+ describe('when isRoot is false', () => {
+ const subject = () => mountSetAppSearchBreadcrumbs({ text: 'Page 1', isRoot: false });
+
+ it('calls appSearchBreadcrumbs to build breadcrumbs, then registers them with Kibana', () => {
+ subject();
+
+ // calls appSearchBreadcrumbs to build breadcrumbs with the target page and current location
+ expect(appSearchBreadCrumbsInnerCall).toHaveBeenCalledWith([
+ { text: 'Page 1', path: '/current-path' },
+ ]);
+
+ // then registers them with Kibana
+ expect(setBreadcrumbs).toHaveBeenCalledWith(builtBreadcrumbs);
+ });
+ });
+
+ describe('when isRoot is true', () => {
+ const subject = () => mountSetAppSearchBreadcrumbs({ text: 'Page 1', isRoot: true });
+
+ it('calls appSearchBreadcrumbs to build breadcrumbs with an empty breadcrumb, then registers them with Kibana', () => {
+ subject();
+
+ // uses an empty bredcrumb
+ expect(appSearchBreadCrumbsInnerCall).toHaveBeenCalledWith([]);
+
+ // then registers them with Kibana
+ expect(setBreadcrumbs).toHaveBeenCalledWith(builtBreadcrumbs);
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.tsx
new file mode 100644
index 0000000000000..530117e197616
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_breadcrumbs/set_breadcrumbs.tsx
@@ -0,0 +1,43 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useContext, useEffect } from 'react';
+import { useHistory } from 'react-router-dom';
+import { EuiBreadcrumb } from '@elastic/eui';
+import { KibanaContext, IKibanaContext } from '../../index';
+import { appSearchBreadcrumbs, TBreadcrumbs } from './generate_breadcrumbs';
+
+/**
+ * Small on-mount helper for setting Kibana's chrome breadcrumbs on any App Search view
+ * @see https://github.com/elastic/kibana/blob/master/src/core/public/chrome/chrome_service.tsx
+ */
+
+export type TSetBreadcrumbs = (breadcrumbs: EuiBreadcrumb[]) => void;
+
+interface IBreadcrumbProps {
+ text: string;
+ isRoot?: never;
+}
+interface IRootBreadcrumbProps {
+ isRoot: true;
+ text?: never;
+}
+
+export const SetAppSearchBreadcrumbs: React.FC = ({
+ text,
+ isRoot,
+}) => {
+ const history = useHistory();
+ const { setBreadcrumbs } = useContext(KibanaContext) as IKibanaContext;
+
+ const crumb = isRoot ? [] : [{ text, path: history.location.pathname }];
+
+ useEffect(() => {
+ setBreadcrumbs(appSearchBreadcrumbs(history)(crumb as TBreadcrumbs | []));
+ }, []);
+
+ return null;
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/index.ts
new file mode 100644
index 0000000000000..9c8c1417d48db
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { LicenseContext, LicenseProvider, ILicenseContext } from './license_context';
+export { hasPlatinumLicense } from './license_checks';
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.test.ts
new file mode 100644
index 0000000000000..ad134e7d36b10
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.test.ts
@@ -0,0 +1,33 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { hasPlatinumLicense } from './license_checks';
+
+describe('hasPlatinumLicense', () => {
+ it('is true for platinum licenses', () => {
+ expect(hasPlatinumLicense({ isActive: true, type: 'platinum' } as any)).toEqual(true);
+ });
+
+ it('is true for enterprise licenses', () => {
+ expect(hasPlatinumLicense({ isActive: true, type: 'enterprise' } as any)).toEqual(true);
+ });
+
+ it('is true for trial licenses', () => {
+ expect(hasPlatinumLicense({ isActive: true, type: 'platinum' } as any)).toEqual(true);
+ });
+
+ it('is false if the current license is expired', () => {
+ expect(hasPlatinumLicense({ isActive: false, type: 'platinum' } as any)).toEqual(false);
+ expect(hasPlatinumLicense({ isActive: false, type: 'enterprise' } as any)).toEqual(false);
+ expect(hasPlatinumLicense({ isActive: false, type: 'trial' } as any)).toEqual(false);
+ });
+
+ it('is false for licenses below platinum', () => {
+ expect(hasPlatinumLicense({ isActive: true, type: 'basic' } as any)).toEqual(false);
+ expect(hasPlatinumLicense({ isActive: false, type: 'standard' } as any)).toEqual(false);
+ expect(hasPlatinumLicense({ isActive: true, type: 'gold' } as any)).toEqual(false);
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.ts b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.ts
new file mode 100644
index 0000000000000..de4a17ce2bd3c
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_checks.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { ILicense } from '../../../../../licensing/public';
+
+export const hasPlatinumLicense = (license?: ILicense) => {
+ return license?.isActive && ['platinum', 'enterprise', 'trial'].includes(license?.type as string);
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.test.tsx
new file mode 100644
index 0000000000000..c65474ec1f590
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.test.tsx
@@ -0,0 +1,24 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useContext } from 'react';
+
+import { mountWithContext } from '../../__mocks__';
+import { LicenseContext, ILicenseContext } from './';
+
+describe('LicenseProvider', () => {
+ const MockComponent: React.FC = () => {
+ const { license } = useContext(LicenseContext) as ILicenseContext;
+ return {license?.type}
;
+ };
+
+ it('renders children', () => {
+ const wrapper = mountWithContext( , { license: { type: 'basic' } });
+
+ expect(wrapper.find('.license-test')).toHaveLength(1);
+ expect(wrapper.text()).toEqual('basic');
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.tsx
new file mode 100644
index 0000000000000..9b47959ff7544
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/licensing/license_context.tsx
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import useObservable from 'react-use/lib/useObservable';
+import { Observable } from 'rxjs';
+
+import { ILicense } from '../../../../../licensing/public';
+
+export interface ILicenseContext {
+ license: ILicense;
+}
+interface ILicenseContextProps {
+ license$: Observable;
+ children: React.ReactNode;
+}
+
+export const LicenseContext = React.createContext({});
+
+export const LicenseProvider: React.FC = ({ license$, children }) => {
+ // Listen for changes to license subscription
+ const license = useObservable(license$);
+
+ // Render rest of application and pass down license via context
+ return ;
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx
new file mode 100644
index 0000000000000..7d4c068b21155
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx
@@ -0,0 +1,77 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { shallow, mount } from 'enzyme';
+import { EuiLink, EuiButton } from '@elastic/eui';
+
+import '../../__mocks__/react_router_history.mock';
+import { mockHistory } from '../../__mocks__';
+
+import { EuiReactRouterLink, EuiReactRouterButton } from './eui_link';
+
+describe('EUI & React Router Component Helpers', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find(EuiLink)).toHaveLength(1);
+ });
+
+ it('renders an EuiButton', () => {
+ const wrapper = shallow( );
+
+ expect(wrapper.find(EuiButton)).toHaveLength(1);
+ });
+
+ it('passes down all ...rest props', () => {
+ const wrapper = shallow( );
+ const link = wrapper.find(EuiLink);
+
+ expect(link.prop('external')).toEqual(true);
+ expect(link.prop('data-test-subj')).toEqual('foo');
+ });
+
+ it('renders with the correct href and onClick props', () => {
+ const wrapper = mount( );
+ const link = wrapper.find(EuiLink);
+
+ expect(link.prop('onClick')).toBeInstanceOf(Function);
+ expect(link.prop('href')).toEqual('/enterprise_search/foo/bar');
+ expect(mockHistory.createHref).toHaveBeenCalled();
+ });
+
+ describe('onClick', () => {
+ it('prevents default navigation and uses React Router history', () => {
+ const wrapper = mount( );
+
+ const simulatedEvent = {
+ button: 0,
+ target: { getAttribute: () => '_self' },
+ preventDefault: jest.fn(),
+ };
+ wrapper.find(EuiLink).simulate('click', simulatedEvent);
+
+ expect(simulatedEvent.preventDefault).toHaveBeenCalled();
+ expect(mockHistory.push).toHaveBeenCalled();
+ });
+
+ it('does not prevent default browser behavior on new tab/window clicks', () => {
+ const wrapper = mount( );
+
+ const simulatedEvent = {
+ shiftKey: true,
+ target: { getAttribute: () => '_blank' },
+ };
+ wrapper.find(EuiLink).simulate('click', simulatedEvent);
+
+ expect(mockHistory.push).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx
new file mode 100644
index 0000000000000..f486e432bae76
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx
@@ -0,0 +1,57 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { useHistory } from 'react-router-dom';
+import { EuiLink, EuiButton, EuiButtonProps, EuiLinkAnchorProps } from '@elastic/eui';
+
+import { letBrowserHandleEvent } from './link_events';
+
+/**
+ * Generates either an EuiLink or EuiButton with a React-Router-ified link
+ *
+ * Based off of EUI's recommendations for handling React Router:
+ * https://github.com/elastic/eui/blob/master/wiki/react-router.md#react-router-51
+ */
+
+interface IEuiReactRouterProps {
+ to: string;
+}
+
+export const EuiReactRouterHelper: React.FC = ({ to, children }) => {
+ const history = useHistory();
+
+ const onClick = (event: React.MouseEvent) => {
+ if (letBrowserHandleEvent(event)) return;
+
+ // Prevent regular link behavior, which causes a browser refresh.
+ event.preventDefault();
+
+ // Push the route to the history.
+ history.push(to);
+ };
+
+ // Generate the correct link href (with basename etc. accounted for)
+ const href = history.createHref({ pathname: to });
+
+ const reactRouterProps = { href, onClick };
+ return React.cloneElement(children as React.ReactElement, reactRouterProps);
+};
+
+type TEuiReactRouterLinkProps = EuiLinkAnchorProps & IEuiReactRouterProps;
+type TEuiReactRouterButtonProps = EuiButtonProps & IEuiReactRouterProps;
+
+export const EuiReactRouterLink: React.FC = ({ to, ...rest }) => (
+
+
+
+);
+
+export const EuiReactRouterButton: React.FC = ({ to, ...rest }) => (
+
+
+
+);
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts
new file mode 100644
index 0000000000000..46dc328633153
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/index.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { letBrowserHandleEvent } from './link_events';
+export { EuiReactRouterLink as EuiLink } from './eui_link';
+export { EuiReactRouterButton as EuiButton } from './eui_link';
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/link_events.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/link_events.test.ts
new file mode 100644
index 0000000000000..3682946b63a13
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/link_events.test.ts
@@ -0,0 +1,102 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { letBrowserHandleEvent } from '../react_router_helpers';
+
+describe('letBrowserHandleEvent', () => {
+ const event = {
+ defaultPrevented: false,
+ metaKey: false,
+ altKey: false,
+ ctrlKey: false,
+ shiftKey: false,
+ button: 0,
+ target: {
+ getAttribute: () => '_self',
+ },
+ } as any;
+
+ describe('the browser should handle the link when', () => {
+ it('default is prevented', () => {
+ expect(letBrowserHandleEvent({ ...event, defaultPrevented: true })).toBe(true);
+ });
+
+ it('is modified with metaKey', () => {
+ expect(letBrowserHandleEvent({ ...event, metaKey: true })).toBe(true);
+ });
+
+ it('is modified with altKey', () => {
+ expect(letBrowserHandleEvent({ ...event, altKey: true })).toBe(true);
+ });
+
+ it('is modified with ctrlKey', () => {
+ expect(letBrowserHandleEvent({ ...event, ctrlKey: true })).toBe(true);
+ });
+
+ it('is modified with shiftKey', () => {
+ expect(letBrowserHandleEvent({ ...event, shiftKey: true })).toBe(true);
+ });
+
+ it('it is not a left click event', () => {
+ expect(letBrowserHandleEvent({ ...event, button: 2 })).toBe(true);
+ });
+
+ it('the target is anything value other than _self', () => {
+ expect(
+ letBrowserHandleEvent({
+ ...event,
+ target: targetValue('_blank'),
+ })
+ ).toBe(true);
+ });
+ });
+
+ describe('the browser should NOT handle the link when', () => {
+ it('default is not prevented', () => {
+ expect(letBrowserHandleEvent({ ...event, defaultPrevented: false })).toBe(false);
+ });
+
+ it('is not modified', () => {
+ expect(
+ letBrowserHandleEvent({
+ ...event,
+ metaKey: false,
+ altKey: false,
+ ctrlKey: false,
+ shiftKey: false,
+ })
+ ).toBe(false);
+ });
+
+ it('it is a left click event', () => {
+ expect(letBrowserHandleEvent({ ...event, button: 0 })).toBe(false);
+ });
+
+ it('the target is a value of _self', () => {
+ expect(
+ letBrowserHandleEvent({
+ ...event,
+ target: targetValue('_self'),
+ })
+ ).toBe(false);
+ });
+
+ it('the target has no value', () => {
+ expect(
+ letBrowserHandleEvent({
+ ...event,
+ target: targetValue(null),
+ })
+ ).toBe(false);
+ });
+ });
+});
+
+const targetValue = (value: string | null) => {
+ return {
+ getAttribute: () => value,
+ };
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/link_events.ts b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/link_events.ts
new file mode 100644
index 0000000000000..93da2ab71d952
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/link_events.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { MouseEvent } from 'react';
+
+/**
+ * Helper functions for determining which events we should
+ * let browsers handle natively, e.g. new tabs/windows
+ */
+
+type THandleEvent = (event: MouseEvent) => boolean;
+
+export const letBrowserHandleEvent: THandleEvent = (event) =>
+ event.defaultPrevented ||
+ isModifiedEvent(event) ||
+ !isLeftClickEvent(event) ||
+ isTargetBlank(event);
+
+const isModifiedEvent: THandleEvent = (event) =>
+ !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
+
+const isLeftClickEvent: THandleEvent = (event) => event.button === 0;
+
+const isTargetBlank: THandleEvent = (event) => {
+ const element = event.target as HTMLElement;
+ const target = element.getAttribute('target');
+ return !!target && target !== '_self';
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/index.ts
new file mode 100644
index 0000000000000..c367424d375f9
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { SetupGuide } from './setup_guide';
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.scss b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.scss
new file mode 100644
index 0000000000000..ecfa13cc828f0
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.scss
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+/**
+ * Setup Guide
+ */
+.setupGuide {
+ padding: 0;
+ min-height: 100vh;
+
+ &__sidebar {
+ flex-basis: $euiSizeXXL * 7.5;
+ flex-shrink: 0;
+ padding: $euiSizeL;
+ margin-right: 0;
+
+ background-color: $euiColorLightestShade;
+ border-color: $euiBorderColor;
+ border-style: solid;
+ border-width: 0 0 $euiBorderWidthThin 0; // bottom - mobile view
+
+ @include euiBreakpoint('m', 'l', 'xl') {
+ border-width: 0 $euiBorderWidthThin 0 0; // right - desktop view
+ }
+ @include euiBreakpoint('m', 'l') {
+ flex-basis: $euiSizeXXL * 10;
+ }
+ @include euiBreakpoint('xl') {
+ flex-basis: $euiSizeXXL * 12.5;
+ }
+ }
+
+ &__body {
+ align-self: start;
+ padding: $euiSizeL;
+
+ @include euiBreakpoint('l') {
+ padding: $euiSizeXXL ($euiSizeXXL * 1.25);
+ }
+ }
+
+ &__thumbnail {
+ display: block;
+ max-width: 100%;
+ height: auto;
+ margin: $euiSizeL auto;
+ }
+}
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.test.tsx
new file mode 100644
index 0000000000000..0423ae61779af
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.test.tsx
@@ -0,0 +1,44 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { shallow } from 'enzyme';
+import { EuiSteps, EuiIcon, EuiLink } from '@elastic/eui';
+
+import { mountWithContext } from '../../__mocks__';
+
+import { SetupGuide } from './';
+
+describe('SetupGuide', () => {
+ it('renders', () => {
+ const wrapper = shallow(
+
+ Wow!
+
+ );
+
+ expect(wrapper.find('h1').text()).toEqual('Enterprise Search');
+ expect(wrapper.find(EuiIcon).prop('type')).toEqual('logoEnterpriseSearch');
+ expect(wrapper.find('[data-test-subj="test"]').text()).toEqual('Wow!');
+ expect(wrapper.find(EuiSteps)).toHaveLength(1);
+ });
+
+ it('renders with optional auth links', () => {
+ const wrapper = mountWithContext(
+
+ Baz
+
+ );
+
+ expect(wrapper.find(EuiLink).first().prop('href')).toEqual('http://bar.com');
+ expect(wrapper.find(EuiLink).last().prop('href')).toEqual('http://foo.com');
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.tsx
new file mode 100644
index 0000000000000..31ff0089dbd7c
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.tsx
@@ -0,0 +1,226 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import {
+ EuiPage,
+ EuiPageSideBar,
+ EuiPageBody,
+ EuiPageContent,
+ EuiSpacer,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiTitle,
+ EuiText,
+ EuiIcon,
+ EuiSteps,
+ EuiCode,
+ EuiCodeBlock,
+ EuiAccordion,
+ EuiLink,
+} from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+
+import './setup_guide.scss';
+
+/**
+ * Shared Setup Guide component. Sidebar content and product name/links are
+ * customizable, but the basic layout and instruction steps are DRYed out
+ */
+
+interface ISetupGuideProps {
+ children: React.ReactNode;
+ productName: string;
+ productEuiIcon: 'logoAppSearch' | 'logoWorkplaceSearch' | 'logoEnterpriseSearch';
+ standardAuthLink?: string;
+ elasticsearchNativeAuthLink?: string;
+}
+
+export const SetupGuide: React.FC = ({
+ children,
+ productName,
+ productEuiIcon,
+ standardAuthLink,
+ elasticsearchNativeAuthLink,
+}) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {productName}
+
+
+
+
+ {children}
+
+
+
+
+
+
+ config/kibana.yml,
+ configSetting: enterpriseSearch.host ,
+ }}
+ />
+
+
+ enterpriseSearch.host: 'http://localhost:3002'
+
+
+ ),
+ },
+ {
+ title: i18n.translate('xpack.enterpriseSearch.setupGuide.step2.title', {
+ defaultMessage: 'Reload your Kibana instance',
+ }),
+ children: (
+
+
+
+
+
+
+ Elasticsearch Native Auth
+
+ ) : (
+ 'Elasticsearch Native Auth'
+ ),
+ }}
+ />
+
+
+ ),
+ },
+ {
+ title: i18n.translate('xpack.enterpriseSearch.setupGuide.step3.title', {
+ defaultMessage: 'Troubleshooting issues',
+ }),
+ children: (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Standard Auth
+
+ ) : (
+ 'Standard Auth'
+ ),
+ }}
+ />
+
+
+
+ >
+ ),
+ },
+ ]}
+ />
+
+
+
+);
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/index.ts
new file mode 100644
index 0000000000000..f871f48b17154
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { sendTelemetry } from './send_telemetry';
+export { SendAppSearchTelemetry } from './send_telemetry';
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx
new file mode 100644
index 0000000000000..9825c0d8ab889
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+
+import { httpServiceMock } from 'src/core/public/mocks';
+import { mountWithKibanaContext } from '../../__mocks__';
+import { sendTelemetry, SendAppSearchTelemetry } from './';
+
+describe('Shared Telemetry Helpers', () => {
+ const httpMock = httpServiceMock.createSetupContract();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('sendTelemetry', () => {
+ it('successfully calls the server-side telemetry endpoint', () => {
+ sendTelemetry({
+ http: httpMock,
+ product: 'enterprise_search',
+ action: 'viewed',
+ metric: 'setup_guide',
+ });
+
+ expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/telemetry', {
+ headers: { 'Content-Type': 'application/json' },
+ body: '{"action":"viewed","metric":"setup_guide"}',
+ });
+ });
+
+ it('throws an error if the telemetry endpoint fails', () => {
+ const httpRejectMock = sendTelemetry({
+ http: { put: () => Promise.reject() },
+ } as any);
+
+ expect(httpRejectMock).rejects.toThrow('Unable to send telemetry');
+ });
+ });
+
+ describe('React component helpers', () => {
+ it('SendAppSearchTelemetry component', () => {
+ mountWithKibanaContext( , {
+ http: httpMock,
+ });
+
+ expect(httpMock.put).toHaveBeenCalledWith('/api/app_search/telemetry', {
+ headers: { 'Content-Type': 'application/json' },
+ body: '{"action":"clicked","metric":"button"}',
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx
new file mode 100644
index 0000000000000..300cb18272717
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx
@@ -0,0 +1,50 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { useContext, useEffect } from 'react';
+
+import { HttpSetup } from 'src/core/public';
+import { KibanaContext, IKibanaContext } from '../../index';
+
+interface ISendTelemetryProps {
+ action: 'viewed' | 'error' | 'clicked';
+ metric: string; // e.g., 'setup_guide'
+}
+
+interface ISendTelemetry extends ISendTelemetryProps {
+ http: HttpSetup;
+ product: 'app_search' | 'workplace_search' | 'enterprise_search';
+}
+
+/**
+ * Base function - useful for non-component actions, e.g. clicks
+ */
+
+export const sendTelemetry = async ({ http, product, action, metric }: ISendTelemetry) => {
+ try {
+ await http.put(`/api/${product}/telemetry`, {
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ action, metric }),
+ });
+ } catch (error) {
+ throw new Error('Unable to send telemetry');
+ }
+};
+
+/**
+ * React component helpers - useful for on-page-load/views
+ * TODO: SendWorkplaceSearchTelemetry and SendEnterpriseSearchTelemetry
+ */
+
+export const SendAppSearchTelemetry: React.FC = ({ action, metric }) => {
+ const { http } = useContext(KibanaContext) as IKibanaContext;
+
+ useEffect(() => {
+ sendTelemetry({ http, action, metric, product: 'app_search' });
+ }, [action, metric, http]);
+
+ return null;
+};
diff --git a/x-pack/plugins/enterprise_search/public/index.ts b/x-pack/plugins/enterprise_search/public/index.ts
new file mode 100644
index 0000000000000..06272641b1929
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { PluginInitializerContext } from 'src/core/public';
+import { EnterpriseSearchPlugin } from './plugin';
+
+export const plugin = (initializerContext: PluginInitializerContext) => {
+ return new EnterpriseSearchPlugin(initializerContext);
+};
diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts
new file mode 100644
index 0000000000000..fbfcc303de47a
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/plugin.ts
@@ -0,0 +1,88 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ Plugin,
+ PluginInitializerContext,
+ CoreSetup,
+ CoreStart,
+ AppMountParameters,
+ HttpSetup,
+} from 'src/core/public';
+
+import {
+ FeatureCatalogueCategory,
+ HomePublicPluginSetup,
+} from '../../../../src/plugins/home/public';
+import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public';
+import { LicensingPluginSetup } from '../../licensing/public';
+
+import { getPublicUrl } from './applications/shared/enterprise_search_url';
+import AppSearchLogo from './applications/app_search/assets/logo.svg';
+
+export interface ClientConfigType {
+ host?: string;
+}
+export interface PluginsSetup {
+ home: HomePublicPluginSetup;
+ licensing: LicensingPluginSetup;
+}
+
+export class EnterpriseSearchPlugin implements Plugin {
+ private config: ClientConfigType;
+ private hasCheckedPublicUrl: boolean = false;
+
+ constructor(initializerContext: PluginInitializerContext) {
+ this.config = initializerContext.config.get();
+ }
+
+ public setup(core: CoreSetup, plugins: PluginsSetup) {
+ const config = { host: this.config.host };
+
+ core.application.register({
+ id: 'appSearch',
+ title: 'App Search',
+ appRoute: '/app/enterprise_search/app_search',
+ category: DEFAULT_APP_CATEGORIES.enterpriseSearch,
+ mount: async (params: AppMountParameters) => {
+ const [coreStart] = await core.getStartServices();
+
+ await this.setPublicUrl(config, coreStart.http);
+
+ const { renderApp } = await import('./applications');
+ const { AppSearch } = await import('./applications/app_search');
+
+ return renderApp(AppSearch, coreStart, params, config, plugins);
+ },
+ });
+ // TODO: Workplace Search will need to register its own plugin.
+
+ plugins.home.featureCatalogue.register({
+ id: 'appSearch',
+ title: 'App Search',
+ icon: AppSearchLogo,
+ description:
+ 'Leverage dashboards, analytics, and APIs for advanced application search made simple.',
+ path: '/app/enterprise_search/app_search',
+ category: FeatureCatalogueCategory.DATA,
+ showOnHomePage: true,
+ });
+ // TODO: Workplace Search will need to register its own feature catalogue section/card.
+ }
+
+ public start(core: CoreStart) {}
+
+ public stop() {}
+
+ private async setPublicUrl(config: ClientConfigType, http: HttpSetup) {
+ if (!config.host) return; // No API to check
+ if (this.hasCheckedPublicUrl) return; // We've already performed the check
+
+ const publicUrl = await getPublicUrl(http);
+ if (publicUrl) config.host = publicUrl;
+ this.hasCheckedPublicUrl = true;
+ }
+}
diff --git a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts
new file mode 100644
index 0000000000000..e95056b871324
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.test.ts
@@ -0,0 +1,143 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { loggingSystemMock } from 'src/core/server/mocks';
+
+jest.mock('../../../../../../src/core/server', () => ({
+ SavedObjectsErrorHelpers: {
+ isNotFoundError: jest.fn(),
+ },
+}));
+import { SavedObjectsErrorHelpers } from '../../../../../../src/core/server';
+
+import { registerTelemetryUsageCollector, incrementUICounter } from './telemetry';
+
+describe('App Search Telemetry Usage Collector', () => {
+ const mockLogger = loggingSystemMock.create().get();
+
+ const makeUsageCollectorStub = jest.fn();
+ const registerStub = jest.fn();
+ const usageCollectionMock = {
+ makeUsageCollector: makeUsageCollectorStub,
+ registerCollector: registerStub,
+ } as any;
+
+ const savedObjectsRepoStub = {
+ get: () => ({
+ attributes: {
+ 'ui_viewed.setup_guide': 10,
+ 'ui_viewed.engines_overview': 20,
+ 'ui_error.cannot_connect': 3,
+ 'ui_clicked.create_first_engine_button': 40,
+ 'ui_clicked.header_launch_button': 50,
+ 'ui_clicked.engine_table_link': 60,
+ },
+ }),
+ incrementCounter: jest.fn(),
+ };
+ const savedObjectsMock = {
+ createInternalRepository: jest.fn(() => savedObjectsRepoStub),
+ } as any;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('registerTelemetryUsageCollector', () => {
+ it('should make and register the usage collector', () => {
+ registerTelemetryUsageCollector(usageCollectionMock, savedObjectsMock, mockLogger);
+
+ expect(registerStub).toHaveBeenCalledTimes(1);
+ expect(makeUsageCollectorStub).toHaveBeenCalledTimes(1);
+ expect(makeUsageCollectorStub.mock.calls[0][0].type).toBe('app_search');
+ expect(makeUsageCollectorStub.mock.calls[0][0].isReady()).toBe(true);
+ });
+ });
+
+ describe('fetchTelemetryMetrics', () => {
+ it('should return existing saved objects data', async () => {
+ registerTelemetryUsageCollector(usageCollectionMock, savedObjectsMock, mockLogger);
+ const savedObjectsCounts = await makeUsageCollectorStub.mock.calls[0][0].fetch();
+
+ expect(savedObjectsCounts).toEqual({
+ ui_viewed: {
+ setup_guide: 10,
+ engines_overview: 20,
+ },
+ ui_error: {
+ cannot_connect: 3,
+ },
+ ui_clicked: {
+ create_first_engine_button: 40,
+ header_launch_button: 50,
+ engine_table_link: 60,
+ },
+ });
+ });
+
+ it('should return a default telemetry object if no saved data exists', async () => {
+ const emptySavedObjectsMock = {
+ createInternalRepository: () => ({
+ get: () => ({ attributes: null }),
+ }),
+ } as any;
+
+ registerTelemetryUsageCollector(usageCollectionMock, emptySavedObjectsMock, mockLogger);
+ const savedObjectsCounts = await makeUsageCollectorStub.mock.calls[0][0].fetch();
+
+ expect(savedObjectsCounts).toEqual({
+ ui_viewed: {
+ setup_guide: 0,
+ engines_overview: 0,
+ },
+ ui_error: {
+ cannot_connect: 0,
+ },
+ ui_clicked: {
+ create_first_engine_button: 0,
+ header_launch_button: 0,
+ engine_table_link: 0,
+ },
+ });
+ });
+
+ it('should not throw but log a warning if saved objects errors', async () => {
+ const errorSavedObjectsMock = { createInternalRepository: () => ({}) } as any;
+ registerTelemetryUsageCollector(usageCollectionMock, errorSavedObjectsMock, mockLogger);
+
+ // Without log warning (not found)
+ (SavedObjectsErrorHelpers.isNotFoundError as jest.Mock).mockImplementationOnce(() => true);
+ await makeUsageCollectorStub.mock.calls[0][0].fetch();
+
+ expect(mockLogger.warn).not.toHaveBeenCalled();
+
+ // With log warning
+ (SavedObjectsErrorHelpers.isNotFoundError as jest.Mock).mockImplementationOnce(() => false);
+ await makeUsageCollectorStub.mock.calls[0][0].fetch();
+
+ expect(mockLogger.warn).toHaveBeenCalledWith(
+ 'Failed to retrieve App Search telemetry data: TypeError: savedObjectsRepository.get is not a function'
+ );
+ });
+ });
+
+ describe('incrementUICounter', () => {
+ it('should increment the saved objects internal repository', async () => {
+ const response = await incrementUICounter({
+ savedObjects: savedObjectsMock,
+ uiAction: 'ui_clicked',
+ metric: 'button',
+ });
+
+ expect(savedObjectsRepoStub.incrementCounter).toHaveBeenCalledWith(
+ 'app_search_telemetry',
+ 'app_search_telemetry',
+ 'ui_clicked.button'
+ );
+ expect(response).toEqual({ success: true });
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts
new file mode 100644
index 0000000000000..a10f96907ad28
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts
@@ -0,0 +1,156 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { get } from 'lodash';
+import {
+ ISavedObjectsRepository,
+ SavedObjectsServiceStart,
+ SavedObjectAttributes,
+ Logger,
+} from 'src/core/server';
+import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
+
+// This throws `Error: Cannot find module 'src/core/server'` if I import it via alias ¯\_(ツ)_/¯
+import { SavedObjectsErrorHelpers } from '../../../../../../src/core/server';
+
+interface ITelemetry {
+ ui_viewed: {
+ setup_guide: number;
+ engines_overview: number;
+ };
+ ui_error: {
+ cannot_connect: number;
+ };
+ ui_clicked: {
+ create_first_engine_button: number;
+ header_launch_button: number;
+ engine_table_link: number;
+ };
+}
+
+export const AS_TELEMETRY_NAME = 'app_search_telemetry';
+
+/**
+ * Register the telemetry collector
+ */
+
+export const registerTelemetryUsageCollector = (
+ usageCollection: UsageCollectionSetup,
+ savedObjects: SavedObjectsServiceStart,
+ log: Logger
+) => {
+ const telemetryUsageCollector = usageCollection.makeUsageCollector({
+ type: 'app_search',
+ fetch: async () => fetchTelemetryMetrics(savedObjects, log),
+ isReady: () => true,
+ schema: {
+ ui_viewed: {
+ setup_guide: { type: 'long' },
+ engines_overview: { type: 'long' },
+ },
+ ui_error: {
+ cannot_connect: { type: 'long' },
+ },
+ ui_clicked: {
+ create_first_engine_button: { type: 'long' },
+ header_launch_button: { type: 'long' },
+ engine_table_link: { type: 'long' },
+ },
+ },
+ });
+ usageCollection.registerCollector(telemetryUsageCollector);
+};
+
+/**
+ * Fetch the aggregated telemetry metrics from our saved objects
+ */
+
+const fetchTelemetryMetrics = async (savedObjects: SavedObjectsServiceStart, log: Logger) => {
+ const savedObjectsRepository = savedObjects.createInternalRepository();
+ const savedObjectAttributes = (await getSavedObjectAttributesFromRepo(
+ savedObjectsRepository,
+ log
+ )) as SavedObjectAttributes;
+
+ const defaultTelemetrySavedObject: ITelemetry = {
+ ui_viewed: {
+ setup_guide: 0,
+ engines_overview: 0,
+ },
+ ui_error: {
+ cannot_connect: 0,
+ },
+ ui_clicked: {
+ create_first_engine_button: 0,
+ header_launch_button: 0,
+ engine_table_link: 0,
+ },
+ };
+
+ // If we don't have an existing/saved telemetry object, return the default
+ if (!savedObjectAttributes) {
+ return defaultTelemetrySavedObject;
+ }
+
+ return {
+ ui_viewed: {
+ setup_guide: get(savedObjectAttributes, 'ui_viewed.setup_guide', 0),
+ engines_overview: get(savedObjectAttributes, 'ui_viewed.engines_overview', 0),
+ },
+ ui_error: {
+ cannot_connect: get(savedObjectAttributes, 'ui_error.cannot_connect', 0),
+ },
+ ui_clicked: {
+ create_first_engine_button: get(
+ savedObjectAttributes,
+ 'ui_clicked.create_first_engine_button',
+ 0
+ ),
+ header_launch_button: get(savedObjectAttributes, 'ui_clicked.header_launch_button', 0),
+ engine_table_link: get(savedObjectAttributes, 'ui_clicked.engine_table_link', 0),
+ },
+ } as ITelemetry;
+};
+
+/**
+ * Helper function - fetches saved objects attributes
+ */
+
+const getSavedObjectAttributesFromRepo = async (
+ savedObjectsRepository: ISavedObjectsRepository,
+ log: Logger
+) => {
+ try {
+ return (await savedObjectsRepository.get(AS_TELEMETRY_NAME, AS_TELEMETRY_NAME)).attributes;
+ } catch (e) {
+ if (!SavedObjectsErrorHelpers.isNotFoundError(e)) {
+ log.warn(`Failed to retrieve App Search telemetry data: ${e}`);
+ }
+ return null;
+ }
+};
+
+/**
+ * Set saved objection attributes - used by telemetry route
+ */
+
+interface IIncrementUICounter {
+ savedObjects: SavedObjectsServiceStart;
+ uiAction: string;
+ metric: string;
+}
+
+export async function incrementUICounter({ savedObjects, uiAction, metric }: IIncrementUICounter) {
+ const internalRepository = savedObjects.createInternalRepository();
+
+ await internalRepository.incrementCounter(
+ AS_TELEMETRY_NAME,
+ AS_TELEMETRY_NAME,
+ `${uiAction}.${metric}` // e.g., ui_viewed.setup_guide
+ );
+
+ return { success: true };
+}
diff --git a/x-pack/plugins/enterprise_search/server/index.ts b/x-pack/plugins/enterprise_search/server/index.ts
new file mode 100644
index 0000000000000..1e4159124ed94
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/index.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server';
+import { schema, TypeOf } from '@kbn/config-schema';
+import { EnterpriseSearchPlugin } from './plugin';
+
+export const plugin = (initializerContext: PluginInitializerContext) => {
+ return new EnterpriseSearchPlugin(initializerContext);
+};
+
+export const configSchema = schema.object({
+ host: schema.maybe(schema.string()),
+ enabled: schema.boolean({ defaultValue: true }),
+ accessCheckTimeout: schema.number({ defaultValue: 5000 }),
+ accessCheckTimeoutWarning: schema.number({ defaultValue: 300 }),
+});
+
+export type ConfigType = TypeOf;
+
+export const config: PluginConfigDescriptor = {
+ schema: configSchema,
+ exposeToBrowser: {
+ host: true,
+ },
+};
diff --git a/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts b/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts
new file mode 100644
index 0000000000000..11d4a387b533f
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts
@@ -0,0 +1,128 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+jest.mock('./enterprise_search_config_api', () => ({
+ callEnterpriseSearchConfigAPI: jest.fn(),
+}));
+import { callEnterpriseSearchConfigAPI } from './enterprise_search_config_api';
+
+import { checkAccess } from './check_access';
+
+describe('checkAccess', () => {
+ const mockSecurity = {
+ authz: {
+ mode: {
+ useRbacForRequest: () => true,
+ },
+ checkPrivilegesWithRequest: () => ({
+ globally: () => ({
+ hasAllRequested: false,
+ }),
+ }),
+ actions: {
+ ui: {
+ get: () => null,
+ },
+ },
+ },
+ };
+ const mockDependencies = {
+ request: {},
+ config: { host: 'http://localhost:3002' },
+ security: mockSecurity,
+ } as any;
+
+ describe('when security is disabled', () => {
+ it('should allow all access', async () => {
+ const security = undefined;
+ expect(await checkAccess({ ...mockDependencies, security })).toEqual({
+ hasAppSearchAccess: true,
+ hasWorkplaceSearchAccess: true,
+ });
+ });
+ });
+
+ describe('when the user is a superuser', () => {
+ it('should allow all access', async () => {
+ const security = {
+ ...mockSecurity,
+ authz: {
+ mode: { useRbacForRequest: () => true },
+ checkPrivilegesWithRequest: () => ({
+ globally: () => ({
+ hasAllRequested: true,
+ }),
+ }),
+ actions: { ui: { get: () => {} } },
+ },
+ };
+ expect(await checkAccess({ ...mockDependencies, security })).toEqual({
+ hasAppSearchAccess: true,
+ hasWorkplaceSearchAccess: true,
+ });
+ });
+
+ it('falls back to assuming a non-superuser role if auth credentials are missing', async () => {
+ const security = {
+ authz: {
+ ...mockSecurity.authz,
+ checkPrivilegesWithRequest: () => ({
+ globally: () => Promise.reject({ statusCode: 403 }),
+ }),
+ },
+ };
+ expect(await checkAccess({ ...mockDependencies, security })).toEqual({
+ hasAppSearchAccess: false,
+ hasWorkplaceSearchAccess: false,
+ });
+ });
+
+ it('throws other authz errors', async () => {
+ const security = {
+ authz: {
+ ...mockSecurity.authz,
+ checkPrivilegesWithRequest: undefined,
+ },
+ };
+ await expect(checkAccess({ ...mockDependencies, security })).rejects.toThrow();
+ });
+ });
+
+ describe('when the user is a non-superuser', () => {
+ describe('when enterpriseSearch.host is not set in kibana.yml', () => {
+ it('should deny all access', async () => {
+ const config = { host: undefined };
+ expect(await checkAccess({ ...mockDependencies, config })).toEqual({
+ hasAppSearchAccess: false,
+ hasWorkplaceSearchAccess: false,
+ });
+ });
+ });
+
+ describe('when enterpriseSearch.host is set in kibana.yml', () => {
+ it('should make a http call and return the access response', async () => {
+ (callEnterpriseSearchConfigAPI as jest.Mock).mockImplementationOnce(() => ({
+ access: {
+ hasAppSearchAccess: false,
+ hasWorkplaceSearchAccess: true,
+ },
+ }));
+ expect(await checkAccess(mockDependencies)).toEqual({
+ hasAppSearchAccess: false,
+ hasWorkplaceSearchAccess: true,
+ });
+ });
+
+ it('falls back to no access if no http response', async () => {
+ (callEnterpriseSearchConfigAPI as jest.Mock).mockImplementationOnce(() => ({}));
+ expect(await checkAccess(mockDependencies)).toEqual({
+ hasAppSearchAccess: false,
+ hasWorkplaceSearchAccess: false,
+ });
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/server/lib/check_access.ts b/x-pack/plugins/enterprise_search/server/lib/check_access.ts
new file mode 100644
index 0000000000000..0239cb6422d03
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/lib/check_access.ts
@@ -0,0 +1,76 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { KibanaRequest, Logger } from 'src/core/server';
+import { SecurityPluginSetup } from '../../../security/server';
+import { ConfigType } from '../';
+
+import { callEnterpriseSearchConfigAPI } from './enterprise_search_config_api';
+
+interface ICheckAccess {
+ request: KibanaRequest;
+ security?: SecurityPluginSetup;
+ config: ConfigType;
+ log: Logger;
+}
+export interface IAccess {
+ hasAppSearchAccess: boolean;
+ hasWorkplaceSearchAccess: boolean;
+}
+
+const ALLOW_ALL_PLUGINS = {
+ hasAppSearchAccess: true,
+ hasWorkplaceSearchAccess: true,
+};
+const DENY_ALL_PLUGINS = {
+ hasAppSearchAccess: false,
+ hasWorkplaceSearchAccess: false,
+};
+
+/**
+ * Determines whether the user has access to our Enterprise Search products
+ * via HTTP call. If not, we hide the corresponding plugin links from the
+ * nav and catalogue in `plugin.ts`, which disables plugin access
+ */
+export const checkAccess = async ({
+ config,
+ security,
+ request,
+ log,
+}: ICheckAccess): Promise => {
+ // If security has been disabled, always show the plugin
+ if (!security?.authz.mode.useRbacForRequest(request)) {
+ return ALLOW_ALL_PLUGINS;
+ }
+
+ // If the user is a "superuser" or has the base Kibana all privilege globally, always show the plugin
+ const isSuperUser = async (): Promise => {
+ try {
+ const { hasAllRequested } = await security.authz
+ .checkPrivilegesWithRequest(request)
+ .globally(security.authz.actions.ui.get('enterpriseSearch', 'all'));
+ return hasAllRequested;
+ } catch (err) {
+ if (err.statusCode === 401 || err.statusCode === 403) {
+ return false;
+ }
+ throw err;
+ }
+ };
+ if (await isSuperUser()) {
+ return ALLOW_ALL_PLUGINS;
+ }
+
+ // Hide the plugin when enterpriseSearch.host is not defined in kibana.yml
+ if (!config.host) {
+ return DENY_ALL_PLUGINS;
+ }
+
+ // When enterpriseSearch.host is defined in kibana.yml,
+ // make a HTTP call which returns product access
+ const { access } = (await callEnterpriseSearchConfigAPI({ request, config, log })) || {};
+ return access || DENY_ALL_PLUGINS;
+};
diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts
new file mode 100644
index 0000000000000..cf35a458b4825
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts
@@ -0,0 +1,111 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+jest.mock('node-fetch');
+const fetchMock = require('node-fetch') as jest.Mock;
+const { Response } = jest.requireActual('node-fetch');
+
+import { loggingSystemMock } from 'src/core/server/mocks';
+
+import { callEnterpriseSearchConfigAPI } from './enterprise_search_config_api';
+
+describe('callEnterpriseSearchConfigAPI', () => {
+ const mockConfig = {
+ host: 'http://localhost:3002',
+ accessCheckTimeout: 200,
+ accessCheckTimeoutWarning: 100,
+ };
+ const mockRequest = {
+ url: { path: '/app/kibana' },
+ headers: { authorization: '==someAuth' },
+ };
+ const mockDependencies = {
+ config: mockConfig,
+ request: mockRequest,
+ log: loggingSystemMock.create().get(),
+ } as any;
+
+ const mockResponse = {
+ version: {
+ number: '1.0.0',
+ },
+ settings: {
+ external_url: 'http://some.vanity.url/',
+ },
+ access: {
+ user: 'someuser',
+ products: {
+ app_search: true,
+ workplace_search: false,
+ },
+ },
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('calls the config API endpoint', async () => {
+ fetchMock.mockImplementationOnce((url: string) => {
+ expect(url).toEqual('http://localhost:3002/api/ent/v1/internal/client_config');
+ return Promise.resolve(new Response(JSON.stringify(mockResponse)));
+ });
+
+ expect(await callEnterpriseSearchConfigAPI(mockDependencies)).toEqual({
+ publicUrl: 'http://some.vanity.url/',
+ access: {
+ hasAppSearchAccess: true,
+ hasWorkplaceSearchAccess: false,
+ },
+ });
+ });
+
+ it('returns early if config.host is not set', async () => {
+ const config = { host: '' };
+
+ expect(await callEnterpriseSearchConfigAPI({ ...mockDependencies, config })).toEqual({});
+ expect(fetchMock).not.toHaveBeenCalled();
+ });
+
+ it('handles server errors', async () => {
+ fetchMock.mockImplementationOnce(() => {
+ return Promise.reject('500');
+ });
+ expect(await callEnterpriseSearchConfigAPI(mockDependencies)).toEqual({});
+ expect(mockDependencies.log.error).toHaveBeenCalledWith(
+ 'Could not perform access check to Enterprise Search: 500'
+ );
+
+ fetchMock.mockImplementationOnce(() => {
+ return Promise.resolve('Bad Data');
+ });
+ expect(await callEnterpriseSearchConfigAPI(mockDependencies)).toEqual({});
+ expect(mockDependencies.log.error).toHaveBeenCalledWith(
+ 'Could not perform access check to Enterprise Search: TypeError: response.json is not a function'
+ );
+ });
+
+ it('handles timeouts', async () => {
+ jest.useFakeTimers();
+
+ // Warning
+ callEnterpriseSearchConfigAPI(mockDependencies);
+ jest.advanceTimersByTime(150);
+ expect(mockDependencies.log.warn).toHaveBeenCalledWith(
+ 'Enterprise Search access check took over 100ms. Please ensure your Enterprise Search server is respondingly normally and not adversely impacting Kibana load speeds.'
+ );
+
+ // Timeout
+ fetchMock.mockImplementationOnce(async () => {
+ jest.advanceTimersByTime(250);
+ return Promise.reject({ name: 'AbortError' });
+ });
+ expect(await callEnterpriseSearchConfigAPI(mockDependencies)).toEqual({});
+ expect(mockDependencies.log.warn).toHaveBeenCalledWith(
+ "Exceeded 200ms timeout while checking http://localhost:3002. Please consider increasing your enterpriseSearch.accessCheckTimeout value so that users aren't prevented from accessing Enterprise Search plugins due to slow responses."
+ );
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts
new file mode 100644
index 0000000000000..7a6d1eac1b454
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts
@@ -0,0 +1,78 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import AbortController from 'abort-controller';
+import fetch from 'node-fetch';
+
+import { KibanaRequest, Logger } from 'src/core/server';
+import { ConfigType } from '../';
+import { IAccess } from './check_access';
+
+interface IParams {
+ request: KibanaRequest;
+ config: ConfigType;
+ log: Logger;
+}
+interface IReturn {
+ publicUrl?: string;
+ access?: IAccess;
+}
+
+/**
+ * Calls an internal Enterprise Search API endpoint which returns
+ * useful various settings (e.g. product access, external URL)
+ * needed by the Kibana plugin at the setup stage
+ */
+const ENDPOINT = '/api/ent/v1/internal/client_config';
+
+export const callEnterpriseSearchConfigAPI = async ({
+ config,
+ log,
+ request,
+}: IParams): Promise => {
+ if (!config.host) return {};
+
+ const TIMEOUT_WARNING = `Enterprise Search access check took over ${config.accessCheckTimeoutWarning}ms. Please ensure your Enterprise Search server is respondingly normally and not adversely impacting Kibana load speeds.`;
+ const TIMEOUT_MESSAGE = `Exceeded ${config.accessCheckTimeout}ms timeout while checking ${config.host}. Please consider increasing your enterpriseSearch.accessCheckTimeout value so that users aren't prevented from accessing Enterprise Search plugins due to slow responses.`;
+ const CONNECTION_ERROR = 'Could not perform access check to Enterprise Search';
+
+ const warningTimeout = setTimeout(() => {
+ log.warn(TIMEOUT_WARNING);
+ }, config.accessCheckTimeoutWarning);
+
+ const controller = new AbortController();
+ const timeout = setTimeout(() => {
+ controller.abort();
+ }, config.accessCheckTimeout);
+
+ try {
+ const enterpriseSearchUrl = encodeURI(`${config.host}${ENDPOINT}`);
+ const response = await fetch(enterpriseSearchUrl, {
+ headers: { Authorization: request.headers.authorization as string },
+ signal: controller.signal,
+ });
+ const data = await response.json();
+
+ return {
+ publicUrl: data?.settings?.external_url,
+ access: {
+ hasAppSearchAccess: !!data?.access?.products?.app_search,
+ hasWorkplaceSearchAccess: !!data?.access?.products?.workplace_search,
+ },
+ };
+ } catch (err) {
+ if (err.name === 'AbortError') {
+ log.warn(TIMEOUT_MESSAGE);
+ } else {
+ log.error(`${CONNECTION_ERROR}: ${err.toString()}`);
+ if (err instanceof Error) log.debug(err.stack as string);
+ }
+ return {};
+ } finally {
+ clearTimeout(warningTimeout);
+ clearTimeout(timeout);
+ }
+};
diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts
new file mode 100644
index 0000000000000..70be8600862e9
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/plugin.ts
@@ -0,0 +1,121 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Observable } from 'rxjs';
+import { first } from 'rxjs/operators';
+import {
+ Plugin,
+ PluginInitializerContext,
+ CoreSetup,
+ Logger,
+ SavedObjectsServiceStart,
+ IRouter,
+ KibanaRequest,
+} from 'src/core/server';
+import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
+import { SecurityPluginSetup } from '../../security/server';
+import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
+
+import { ConfigType } from './';
+import { checkAccess } from './lib/check_access';
+import { registerPublicUrlRoute } from './routes/enterprise_search/public_url';
+import { registerEnginesRoute } from './routes/app_search/engines';
+import { registerTelemetryRoute } from './routes/app_search/telemetry';
+import { registerTelemetryUsageCollector } from './collectors/app_search/telemetry';
+import { appSearchTelemetryType } from './saved_objects/app_search/telemetry';
+
+export interface PluginsSetup {
+ usageCollection?: UsageCollectionSetup;
+ security?: SecurityPluginSetup;
+ features: FeaturesPluginSetup;
+}
+
+export interface IRouteDependencies {
+ router: IRouter;
+ config: ConfigType;
+ log: Logger;
+ getSavedObjectsService?(): SavedObjectsServiceStart;
+}
+
+export class EnterpriseSearchPlugin implements Plugin {
+ private config: Observable;
+ private logger: Logger;
+
+ constructor(initializerContext: PluginInitializerContext) {
+ this.config = initializerContext.config.create();
+ this.logger = initializerContext.logger.get();
+ }
+
+ public async setup(
+ { capabilities, http, savedObjects, getStartServices }: CoreSetup,
+ { usageCollection, security, features }: PluginsSetup
+ ) {
+ const config = await this.config.pipe(first()).toPromise();
+
+ /**
+ * Register space/feature control
+ */
+ features.registerFeature({
+ id: 'enterpriseSearch',
+ name: 'Enterprise Search',
+ order: 0,
+ icon: 'logoEnterpriseSearch',
+ navLinkId: 'appSearch', // TODO - remove this once functional tests no longer rely on navLinkId
+ app: ['kibana', 'appSearch'], // TODO: 'enterpriseSearch', 'workplaceSearch'
+ catalogue: ['appSearch'], // TODO: 'enterpriseSearch', 'workplaceSearch'
+ privileges: null,
+ });
+
+ /**
+ * Register user access to the Enterprise Search plugins
+ */
+ capabilities.registerSwitcher(async (request: KibanaRequest) => {
+ const dependencies = { config, security, request, log: this.logger };
+
+ const { hasAppSearchAccess } = await checkAccess(dependencies);
+ // TODO: hasWorkplaceSearchAccess
+
+ return {
+ navLinks: {
+ appSearch: hasAppSearchAccess,
+ },
+ catalogue: {
+ appSearch: hasAppSearchAccess,
+ },
+ };
+ });
+
+ /**
+ * Register routes
+ */
+ const router = http.createRouter();
+ const dependencies = { router, config, log: this.logger };
+
+ registerPublicUrlRoute(dependencies);
+ registerEnginesRoute(dependencies);
+
+ /**
+ * Bootstrap the routes, saved objects, and collector for telemetry
+ */
+ savedObjects.registerType(appSearchTelemetryType);
+ let savedObjectsStarted: SavedObjectsServiceStart;
+
+ getStartServices().then(([coreStart]) => {
+ savedObjectsStarted = coreStart.savedObjects;
+ if (usageCollection) {
+ registerTelemetryUsageCollector(usageCollection, savedObjectsStarted, this.logger);
+ }
+ });
+ registerTelemetryRoute({
+ ...dependencies,
+ getSavedObjectsService: () => savedObjectsStarted,
+ });
+ }
+
+ public start() {}
+
+ public stop() {}
+}
diff --git a/x-pack/plugins/enterprise_search/server/routes/__mocks__/index.ts b/x-pack/plugins/enterprise_search/server/routes/__mocks__/index.ts
new file mode 100644
index 0000000000000..3cca5e21ce9c3
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/routes/__mocks__/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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { MockRouter } from './router.mock';
+export { mockConfig, mockLogger, mockDependencies } from './routerDependencies.mock';
diff --git a/x-pack/plugins/enterprise_search/server/routes/__mocks__/router.mock.ts b/x-pack/plugins/enterprise_search/server/routes/__mocks__/router.mock.ts
new file mode 100644
index 0000000000000..1ca7755979f99
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/routes/__mocks__/router.mock.ts
@@ -0,0 +1,102 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { httpServiceMock, httpServerMock } from 'src/core/server/mocks';
+import {
+ IRouter,
+ KibanaRequest,
+ RequestHandlerContext,
+ RouteValidatorConfig,
+} from 'src/core/server';
+
+/**
+ * Test helper that mocks Kibana's router and DRYs out various helper (callRoute, schema validation)
+ */
+
+type methodType = 'get' | 'post' | 'put' | 'patch' | 'delete';
+type payloadType = 'params' | 'query' | 'body';
+
+interface IMockRouterProps {
+ method: methodType;
+ payload?: payloadType;
+}
+interface IMockRouterRequest {
+ body?: object;
+ query?: object;
+ params?: object;
+}
+type TMockRouterRequest = KibanaRequest | IMockRouterRequest;
+
+export class MockRouter {
+ public router!: jest.Mocked;
+ public method: methodType;
+ public payload?: payloadType;
+ public response = httpServerMock.createResponseFactory();
+
+ constructor({ method, payload }: IMockRouterProps) {
+ this.createRouter();
+ this.method = method;
+ this.payload = payload;
+ }
+
+ public createRouter = () => {
+ this.router = httpServiceMock.createRouter();
+ };
+
+ public callRoute = async (request: TMockRouterRequest) => {
+ const [, handler] = this.router[this.method].mock.calls[0];
+
+ const context = {} as jest.Mocked;
+ await handler(context, httpServerMock.createKibanaRequest(request as any), this.response);
+ };
+
+ /**
+ * Schema validation helpers
+ */
+
+ public validateRoute = (request: TMockRouterRequest) => {
+ if (!this.payload) throw new Error('Cannot validate wihout a payload type specified.');
+
+ const [config] = this.router[this.method].mock.calls[0];
+ const validate = config.validate as RouteValidatorConfig<{}, {}, {}>;
+
+ const payloadValidation = validate[this.payload] as { validate(request: KibanaRequest): void };
+ const payloadRequest = request[this.payload] as KibanaRequest;
+
+ payloadValidation.validate(payloadRequest);
+ };
+
+ public shouldValidate = (request: TMockRouterRequest) => {
+ expect(() => this.validateRoute(request)).not.toThrow();
+ };
+
+ public shouldThrow = (request: TMockRouterRequest) => {
+ expect(() => this.validateRoute(request)).toThrow();
+ };
+}
+
+/**
+ * Example usage:
+ */
+// const mockRouter = new MockRouter({ method: 'get', payload: 'body' });
+//
+// beforeEach(() => {
+// jest.clearAllMocks();
+// mockRouter.createRouter();
+//
+// registerExampleRoute({ router: mockRouter.router, ...dependencies }); // Whatever other dependencies the route needs
+// });
+
+// it('hits the endpoint successfully', async () => {
+// await mockRouter.callRoute({ body: { foo: 'bar' } });
+//
+// expect(mockRouter.response.ok).toHaveBeenCalled();
+// });
+
+// it('validates', () => {
+// const request = { body: { foo: 'bar' } };
+// mockRouter.shouldValidate(request);
+// });
diff --git a/x-pack/plugins/enterprise_search/server/routes/__mocks__/routerDependencies.mock.ts b/x-pack/plugins/enterprise_search/server/routes/__mocks__/routerDependencies.mock.ts
new file mode 100644
index 0000000000000..9b6fa30271d61
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/routes/__mocks__/routerDependencies.mock.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { loggingSystemMock } from 'src/core/server/mocks';
+import { ConfigType } from '../../';
+
+export const mockLogger = loggingSystemMock.createLogger().get();
+
+export const mockConfig = {
+ enabled: true,
+ host: 'http://localhost:3002',
+ accessCheckTimeout: 5000,
+ accessCheckTimeoutWarning: 300,
+} as ConfigType;
+
+/**
+ * This is useful for tests that don't use either config or log,
+ * but should still pass them in to pass Typescript definitions
+ */
+export const mockDependencies = {
+ // Mock router should be handled on a per-test basis
+ config: mockConfig,
+ log: mockLogger,
+};
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts
new file mode 100644
index 0000000000000..d5b1bc5003456
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts
@@ -0,0 +1,160 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { MockRouter, mockConfig, mockLogger } from '../__mocks__';
+
+import { registerEnginesRoute } from './engines';
+
+jest.mock('node-fetch');
+const fetch = jest.requireActual('node-fetch');
+const { Response } = fetch;
+const fetchMock = require('node-fetch') as jest.Mocked;
+
+describe('engine routes', () => {
+ describe('GET /api/app_search/engines', () => {
+ const AUTH_HEADER = 'Basic 123';
+ const mockRequest = {
+ headers: {
+ authorization: AUTH_HEADER,
+ },
+ query: {
+ type: 'indexed',
+ pageIndex: 1,
+ },
+ };
+
+ let mockRouter: MockRouter;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockRouter = new MockRouter({ method: 'get', payload: 'query' });
+
+ registerEnginesRoute({
+ router: mockRouter.router,
+ log: mockLogger,
+ config: mockConfig,
+ });
+ });
+
+ describe('when the underlying App Search API returns a 200', () => {
+ beforeEach(() => {
+ AppSearchAPI.shouldBeCalledWith(
+ `http://localhost:3002/as/engines/collection?type=indexed&page%5Bcurrent%5D=1&page%5Bsize%5D=10`,
+ { headers: { Authorization: AUTH_HEADER } }
+ ).andReturn({
+ results: [{ name: 'engine1' }],
+ meta: { page: { total_results: 1 } },
+ });
+ });
+
+ it('should return 200 with a list of engines from the App Search API', async () => {
+ await mockRouter.callRoute(mockRequest);
+
+ expect(mockRouter.response.ok).toHaveBeenCalledWith({
+ body: { results: [{ name: 'engine1' }], meta: { page: { total_results: 1 } } },
+ });
+ });
+ });
+
+ describe('when the App Search URL is invalid', () => {
+ beforeEach(() => {
+ AppSearchAPI.shouldBeCalledWith(
+ `http://localhost:3002/as/engines/collection?type=indexed&page%5Bcurrent%5D=1&page%5Bsize%5D=10`,
+ { headers: { Authorization: AUTH_HEADER } }
+ ).andReturnError();
+ });
+
+ it('should return 404 with a message', async () => {
+ await mockRouter.callRoute(mockRequest);
+
+ expect(mockRouter.response.notFound).toHaveBeenCalledWith({
+ body: 'cannot-connect',
+ });
+ expect(mockLogger.error).toHaveBeenCalledWith('Cannot connect to App Search: Failed');
+ expect(mockLogger.debug).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when the App Search API returns invalid data', () => {
+ beforeEach(() => {
+ AppSearchAPI.shouldBeCalledWith(
+ `http://localhost:3002/as/engines/collection?type=indexed&page%5Bcurrent%5D=1&page%5Bsize%5D=10`,
+ { headers: { Authorization: AUTH_HEADER } }
+ ).andReturnInvalidData();
+ });
+
+ it('should return 404 with a message', async () => {
+ await mockRouter.callRoute(mockRequest);
+
+ expect(mockRouter.response.notFound).toHaveBeenCalledWith({
+ body: 'cannot-connect',
+ });
+ expect(mockLogger.error).toHaveBeenCalledWith(
+ 'Cannot connect to App Search: Error: Invalid data received from App Search: {"foo":"bar"}'
+ );
+ expect(mockLogger.debug).toHaveBeenCalled();
+ });
+ });
+
+ describe('validates', () => {
+ it('correctly', () => {
+ const request = { query: { type: 'meta', pageIndex: 5 } };
+ mockRouter.shouldValidate(request);
+ });
+
+ it('wrong pageIndex type', () => {
+ const request = { query: { type: 'indexed', pageIndex: 'indexed' } };
+ mockRouter.shouldThrow(request);
+ });
+
+ it('wrong type string', () => {
+ const request = { query: { type: 'invalid', pageIndex: 1 } };
+ mockRouter.shouldThrow(request);
+ });
+
+ it('missing pageIndex', () => {
+ const request = { query: { type: 'indexed' } };
+ mockRouter.shouldThrow(request);
+ });
+
+ it('missing type', () => {
+ const request = { query: { pageIndex: 1 } };
+ mockRouter.shouldThrow(request);
+ });
+ });
+
+ const AppSearchAPI = {
+ shouldBeCalledWith(expectedUrl: string, expectedParams: object) {
+ return {
+ andReturn(response: object) {
+ fetchMock.mockImplementation((url: string, params: object) => {
+ expect(url).toEqual(expectedUrl);
+ expect(params).toEqual(expectedParams);
+
+ return Promise.resolve(new Response(JSON.stringify(response)));
+ });
+ },
+ andReturnInvalidData() {
+ fetchMock.mockImplementation((url: string, params: object) => {
+ expect(url).toEqual(expectedUrl);
+ expect(params).toEqual(expectedParams);
+
+ return Promise.resolve(new Response(JSON.stringify({ foo: 'bar' })));
+ });
+ },
+ andReturnError() {
+ fetchMock.mockImplementation((url: string, params: object) => {
+ expect(url).toEqual(expectedUrl);
+ expect(params).toEqual(expectedParams);
+
+ return Promise.reject('Failed');
+ });
+ },
+ };
+ },
+ };
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts
new file mode 100644
index 0000000000000..ca83c0e187ddb
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts
@@ -0,0 +1,59 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import fetch from 'node-fetch';
+import querystring from 'querystring';
+import { schema } from '@kbn/config-schema';
+
+import { IRouteDependencies } from '../../plugin';
+import { ENGINES_PAGE_SIZE } from '../../../common/constants';
+
+export function registerEnginesRoute({ router, config, log }: IRouteDependencies) {
+ router.get(
+ {
+ path: '/api/app_search/engines',
+ validate: {
+ query: schema.object({
+ type: schema.oneOf([schema.literal('indexed'), schema.literal('meta')]),
+ pageIndex: schema.number(),
+ }),
+ },
+ },
+ async (context, request, response) => {
+ try {
+ const enterpriseSearchUrl = config.host as string;
+ const { type, pageIndex } = request.query;
+
+ const params = querystring.stringify({
+ type,
+ 'page[current]': pageIndex,
+ 'page[size]': ENGINES_PAGE_SIZE,
+ });
+ const url = `${encodeURI(enterpriseSearchUrl)}/as/engines/collection?${params}`;
+
+ const enginesResponse = await fetch(url, {
+ headers: { Authorization: request.headers.authorization as string },
+ });
+
+ const engines = await enginesResponse.json();
+ const hasValidData =
+ Array.isArray(engines?.results) && typeof engines?.meta?.page?.total_results === 'number';
+
+ if (hasValidData) {
+ return response.ok({ body: engines });
+ } else {
+ // Either a completely incorrect Enterprise Search host URL was configured, or App Search is returning bad data
+ throw new Error(`Invalid data received from App Search: ${JSON.stringify(engines)}`);
+ }
+ } catch (e) {
+ log.error(`Cannot connect to App Search: ${e.toString()}`);
+ if (e instanceof Error) log.debug(e.stack as string);
+
+ return response.notFound({ body: 'cannot-connect' });
+ }
+ }
+ );
+}
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/telemetry.test.ts
new file mode 100644
index 0000000000000..e2d5fbcec3705
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/telemetry.test.ts
@@ -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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks';
+import { MockRouter, mockConfig, mockLogger } from '../__mocks__';
+
+import { registerTelemetryRoute } from './telemetry';
+
+jest.mock('../../collectors/app_search/telemetry', () => ({
+ incrementUICounter: jest.fn(),
+}));
+import { incrementUICounter } from '../../collectors/app_search/telemetry';
+
+/**
+ * Since these route callbacks are so thin, these serve simply as integration tests
+ * to ensure they're wired up to the collector functions correctly. Business logic
+ * is tested more thoroughly in the collectors/telemetry tests.
+ */
+describe('App Search Telemetry API', () => {
+ let mockRouter: MockRouter;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockRouter = new MockRouter({ method: 'put', payload: 'body' });
+
+ registerTelemetryRoute({
+ router: mockRouter.router,
+ getSavedObjectsService: () => savedObjectsServiceMock.createStartContract(),
+ log: mockLogger,
+ config: mockConfig,
+ });
+ });
+
+ describe('PUT /api/app_search/telemetry', () => {
+ it('increments the saved objects counter', async () => {
+ const successResponse = { success: true };
+ (incrementUICounter as jest.Mock).mockImplementation(jest.fn(() => successResponse));
+
+ await mockRouter.callRoute({ body: { action: 'viewed', metric: 'setup_guide' } });
+
+ expect(incrementUICounter).toHaveBeenCalledWith({
+ savedObjects: expect.any(Object),
+ uiAction: 'ui_viewed',
+ metric: 'setup_guide',
+ });
+ expect(mockRouter.response.ok).toHaveBeenCalledWith({ body: successResponse });
+ });
+
+ it('throws an error when incrementing fails', async () => {
+ (incrementUICounter as jest.Mock).mockImplementation(jest.fn(() => Promise.reject('Failed')));
+
+ await mockRouter.callRoute({ body: { action: 'error', metric: 'error' } });
+
+ expect(incrementUICounter).toHaveBeenCalled();
+ expect(mockLogger.error).toHaveBeenCalled();
+ expect(mockRouter.response.internalError).toHaveBeenCalled();
+ });
+
+ it('throws an error if the Saved Objects service is unavailable', async () => {
+ jest.clearAllMocks();
+ registerTelemetryRoute({
+ router: mockRouter.router,
+ getSavedObjectsService: null,
+ log: mockLogger,
+ } as any);
+ await mockRouter.callRoute({});
+
+ expect(incrementUICounter).not.toHaveBeenCalled();
+ expect(mockLogger.error).toHaveBeenCalled();
+ expect(mockRouter.response.internalError).toHaveBeenCalled();
+ expect(loggingSystemMock.collect(mockLogger).error[0][0]).toEqual(
+ expect.stringContaining(
+ 'App Search UI telemetry error: Error: Could not find Saved Objects service'
+ )
+ );
+ });
+
+ describe('validates', () => {
+ it('correctly', () => {
+ const request = { body: { action: 'viewed', metric: 'setup_guide' } };
+ mockRouter.shouldValidate(request);
+ });
+
+ it('wrong action string', () => {
+ const request = { body: { action: 'invalid', metric: 'setup_guide' } };
+ mockRouter.shouldThrow(request);
+ });
+
+ it('wrong metric type', () => {
+ const request = { body: { action: 'clicked', metric: true } };
+ mockRouter.shouldThrow(request);
+ });
+
+ it('action is missing', () => {
+ const request = { body: { metric: 'engines_overview' } };
+ mockRouter.shouldThrow(request);
+ });
+
+ it('metric is missing', () => {
+ const request = { body: { action: 'error' } };
+ mockRouter.shouldThrow(request);
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/telemetry.ts
new file mode 100644
index 0000000000000..4cc9b64adc092
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/telemetry.ts
@@ -0,0 +1,50 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { schema } from '@kbn/config-schema';
+
+import { IRouteDependencies } from '../../plugin';
+import { incrementUICounter } from '../../collectors/app_search/telemetry';
+
+export function registerTelemetryRoute({
+ router,
+ getSavedObjectsService,
+ log,
+}: IRouteDependencies) {
+ router.put(
+ {
+ path: '/api/app_search/telemetry',
+ validate: {
+ body: schema.object({
+ action: schema.oneOf([
+ schema.literal('viewed'),
+ schema.literal('clicked'),
+ schema.literal('error'),
+ ]),
+ metric: schema.string(),
+ }),
+ },
+ },
+ async (ctx, request, response) => {
+ const { action, metric } = request.body;
+
+ try {
+ if (!getSavedObjectsService) throw new Error('Could not find Saved Objects service');
+
+ return response.ok({
+ body: await incrementUICounter({
+ savedObjects: getSavedObjectsService(),
+ uiAction: `ui_${action}`,
+ metric,
+ }),
+ });
+ } catch (e) {
+ log.error(`App Search UI telemetry error: ${e instanceof Error ? e.stack : e.toString()}`);
+ return response.internalError({ body: 'App Search UI telemetry failed' });
+ }
+ }
+ );
+}
diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.test.ts
new file mode 100644
index 0000000000000..846aae3fce56f
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.test.ts
@@ -0,0 +1,52 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { MockRouter, mockDependencies } from '../__mocks__';
+
+jest.mock('../../lib/enterprise_search_config_api', () => ({
+ callEnterpriseSearchConfigAPI: jest.fn(),
+}));
+import { callEnterpriseSearchConfigAPI } from '../../lib/enterprise_search_config_api';
+
+import { registerPublicUrlRoute } from './public_url';
+
+describe('Enterprise Search Public URL API', () => {
+ let mockRouter: MockRouter;
+
+ beforeEach(() => {
+ mockRouter = new MockRouter({ method: 'get' });
+
+ registerPublicUrlRoute({
+ ...mockDependencies,
+ router: mockRouter.router,
+ });
+ });
+
+ describe('GET /api/enterprise_search/public_url', () => {
+ it('returns a publicUrl', async () => {
+ (callEnterpriseSearchConfigAPI as jest.Mock).mockImplementationOnce(() => {
+ return Promise.resolve({ publicUrl: 'http://some.vanity.url' });
+ });
+
+ await mockRouter.callRoute({});
+
+ expect(mockRouter.response.ok).toHaveBeenCalledWith({
+ body: { publicUrl: 'http://some.vanity.url' },
+ headers: { 'content-type': 'application/json' },
+ });
+ });
+
+ // For the most part, all error logging is handled by callEnterpriseSearchConfigAPI.
+ // This endpoint should mostly just fall back gracefully to an empty string
+ it('falls back to an empty string', async () => {
+ await mockRouter.callRoute({});
+ expect(mockRouter.response.ok).toHaveBeenCalledWith({
+ body: { publicUrl: '' },
+ headers: { 'content-type': 'application/json' },
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.ts
new file mode 100644
index 0000000000000..a9edd4eb10da0
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/public_url.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { IRouteDependencies } from '../../plugin';
+import { callEnterpriseSearchConfigAPI } from '../../lib/enterprise_search_config_api';
+
+export function registerPublicUrlRoute({ router, config, log }: IRouteDependencies) {
+ router.get(
+ {
+ path: '/api/enterprise_search/public_url',
+ validate: false,
+ },
+ async (context, request, response) => {
+ const { publicUrl = '' } =
+ (await callEnterpriseSearchConfigAPI({ request, config, log })) || {};
+
+ return response.ok({
+ body: { publicUrl },
+ headers: { 'content-type': 'application/json' },
+ });
+ }
+ );
+}
diff --git a/x-pack/plugins/enterprise_search/server/saved_objects/app_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/saved_objects/app_search/telemetry.ts
new file mode 100644
index 0000000000000..32322d494b5e2
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/server/saved_objects/app_search/telemetry.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+/* istanbul ignore file */
+
+import { SavedObjectsType } from 'src/core/server';
+import { AS_TELEMETRY_NAME } from '../../collectors/app_search/telemetry';
+
+export const appSearchTelemetryType: SavedObjectsType = {
+ name: AS_TELEMETRY_NAME,
+ hidden: false,
+ namespaceType: 'agnostic',
+ mappings: {
+ dynamic: false,
+ properties: {},
+ },
+};
diff --git a/x-pack/plugins/graph/kibana.json b/x-pack/plugins/graph/kibana.json
index ebe18dba2b58c..4e653393100c9 100644
--- a/x-pack/plugins/graph/kibana.json
+++ b/x-pack/plugins/graph/kibana.json
@@ -6,5 +6,6 @@
"ui": true,
"requiredPlugins": ["licensing", "data", "navigation", "savedObjects", "kibanaLegacy"],
"optionalPlugins": ["home", "features"],
- "configPath": ["xpack", "graph"]
+ "configPath": ["xpack", "graph"],
+ "requiredBundles": ["kibanaUtils", "kibanaReact", "home"]
}
diff --git a/x-pack/plugins/grokdebugger/kibana.json b/x-pack/plugins/grokdebugger/kibana.json
index 4d37f9ccdb0de..8466c191ed9b6 100644
--- a/x-pack/plugins/grokdebugger/kibana.json
+++ b/x-pack/plugins/grokdebugger/kibana.json
@@ -9,5 +9,8 @@
],
"server": true,
"ui": true,
- "configPath": ["xpack", "grokdebugger"]
+ "configPath": ["xpack", "grokdebugger"],
+ "requiredBundles": [
+ "kibanaReact"
+ ]
}
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts
index 225432375dc75..e5037a6477aca 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/constants.ts
@@ -5,6 +5,8 @@
*/
export const POLICY_NAME = 'my_policy';
+export const SNAPSHOT_POLICY_NAME = 'my_snapshot_policy';
+export const NEW_SNAPSHOT_POLICY_NAME = 'my_new_snapshot_policy';
export const DELETE_PHASE_POLICY = {
version: 1,
@@ -26,7 +28,7 @@ export const DELETE_PHASE_POLICY = {
min_age: '0ms',
actions: {
wait_for_snapshot: {
- policy: 'my_snapshot_policy',
+ policy: SNAPSHOT_POLICY_NAME,
},
delete: {
delete_searchable_snapshot: true,
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx
index d6c955e0c0813..cba496ee0f212 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx
@@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import React from 'react';
import { act } from 'react-dom/test-utils';
import { registerTestBed, TestBed, TestBedConfig } from '../../../../../test_utils';
@@ -14,6 +15,25 @@ import { TestSubjects } from '../helpers';
import { EditPolicy } from '../../../public/application/sections/edit_policy';
import { indexLifecycleManagementStore } from '../../../public/application/store';
+jest.mock('@elastic/eui', () => {
+ const original = jest.requireActual('@elastic/eui');
+
+ return {
+ ...original,
+ // Mocking EuiComboBox, as it utilizes "react-virtualized" for rendering search suggestions,
+ // which does not produce a valid component wrapper
+ EuiComboBox: (props: any) => (
+ {
+ props.onChange([syntheticEvent['0']]);
+ }}
+ />
+ ),
+ };
+});
+
const testBedConfig: TestBedConfig = {
store: () => indexLifecycleManagementStore(),
memoryRouter: {
@@ -34,9 +54,11 @@ export interface EditPolicyTestBed extends TestBed {
export const setup = async (): Promise => {
const testBed = await initTestBed();
- const setWaitForSnapshotPolicy = (snapshotPolicyName: string) => {
- const { component, form } = testBed;
- form.setInputValue('waitForSnapshotField', snapshotPolicyName, true);
+ const setWaitForSnapshotPolicy = async (snapshotPolicyName: string) => {
+ const { component } = testBed;
+ act(() => {
+ testBed.find('snapshotPolicyCombobox').simulate('change', [{ label: snapshotPolicyName }]);
+ });
component.update();
};
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts
index 8753f01376d42..06829e6ef6f1e 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts
@@ -7,11 +7,10 @@
import { act } from 'react-dom/test-utils';
import { setupEnvironment } from '../helpers/setup_environment';
-
import { EditPolicyTestBed, setup } from './edit_policy.helpers';
-import { DELETE_PHASE_POLICY } from './constants';
import { API_BASE_PATH } from '../../../common/constants';
+import { DELETE_PHASE_POLICY, NEW_SNAPSHOT_POLICY_NAME, SNAPSHOT_POLICY_NAME } from './constants';
window.scrollTo = jest.fn();
@@ -25,6 +24,10 @@ describe(' ', () => {
describe('delete phase', () => {
beforeEach(async () => {
httpRequestsMockHelpers.setLoadPolicies([DELETE_PHASE_POLICY]);
+ httpRequestsMockHelpers.setLoadSnapshotPolicies([
+ SNAPSHOT_POLICY_NAME,
+ NEW_SNAPSHOT_POLICY_NAME,
+ ]);
await act(async () => {
testBed = await setup();
@@ -35,16 +38,18 @@ describe(' ', () => {
});
test('wait for snapshot policy field should correctly display snapshot policy name', () => {
- expect(testBed.find('waitForSnapshotField').props().value).toEqual(
- DELETE_PHASE_POLICY.policy.phases.delete.actions.wait_for_snapshot.policy
- );
+ expect(testBed.find('snapshotPolicyCombobox').prop('data-currentvalue')).toEqual([
+ {
+ label: DELETE_PHASE_POLICY.policy.phases.delete.actions.wait_for_snapshot.policy,
+ value: DELETE_PHASE_POLICY.policy.phases.delete.actions.wait_for_snapshot.policy,
+ },
+ ]);
});
test('wait for snapshot field should correctly update snapshot policy name', async () => {
const { actions } = testBed;
- const newPolicyName = 'my_new_snapshot_policy';
- actions.setWaitForSnapshotPolicy(newPolicyName);
+ await actions.setWaitForSnapshotPolicy(NEW_SNAPSHOT_POLICY_NAME);
await actions.savePolicy();
const expected = {
@@ -56,7 +61,7 @@ describe(' ', () => {
actions: {
...DELETE_PHASE_POLICY.policy.phases.delete.actions,
wait_for_snapshot: {
- policy: newPolicyName,
+ policy: NEW_SNAPSHOT_POLICY_NAME,
},
},
},
@@ -69,6 +74,15 @@ describe(' ', () => {
expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected);
});
+ test('wait for snapshot field should display a callout when the input is not an existing policy', async () => {
+ const { actions } = testBed;
+
+ await actions.setWaitForSnapshotPolicy('my_custom_policy');
+ expect(testBed.find('noPoliciesCallout').exists()).toBeFalsy();
+ expect(testBed.find('policiesErrorCallout').exists()).toBeFalsy();
+ expect(testBed.find('customPolicyCallout').exists()).toBeTruthy();
+ });
+
test('wait for snapshot field should delete action if field is empty', async () => {
const { actions } = testBed;
@@ -92,5 +106,31 @@ describe(' ', () => {
const latestRequest = server.requests[server.requests.length - 1];
expect(JSON.parse(JSON.parse(latestRequest.requestBody).body)).toEqual(expected);
});
+
+ test('wait for snapshot field should display a callout when there are no snapshot policies', async () => {
+ // need to call setup on testBed again for it to use a newly defined snapshot policies response
+ httpRequestsMockHelpers.setLoadSnapshotPolicies([]);
+ await act(async () => {
+ testBed = await setup();
+ });
+
+ testBed.component.update();
+ expect(testBed.find('customPolicyCallout').exists()).toBeFalsy();
+ expect(testBed.find('policiesErrorCallout').exists()).toBeFalsy();
+ expect(testBed.find('noPoliciesCallout').exists()).toBeTruthy();
+ });
+
+ test('wait for snapshot field should display a callout when there is an error loading snapshot policies', async () => {
+ // need to call setup on testBed again for it to use a newly defined snapshot policies response
+ httpRequestsMockHelpers.setLoadSnapshotPolicies([], { status: 500, body: 'error' });
+ await act(async () => {
+ testBed = await setup();
+ });
+
+ testBed.component.update();
+ expect(testBed.find('customPolicyCallout').exists()).toBeFalsy();
+ expect(testBed.find('noPoliciesCallout').exists()).toBeFalsy();
+ expect(testBed.find('policiesErrorCallout').exists()).toBeTruthy();
+ });
});
});
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts
index f41742fc104ff..04f58f93939ca 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/http_requests.ts
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { SinonFakeServer, fakeServer } from 'sinon';
+import { fakeServer, SinonFakeServer } from 'sinon';
import { API_BASE_PATH } from '../../../common/constants';
export const init = () => {
@@ -27,7 +27,19 @@ const registerHttpRequestMockHelpers = (server: SinonFakeServer) => {
]);
};
+ const setLoadSnapshotPolicies = (response: any = [], error?: { status: number; body: any }) => {
+ const status = error ? error.status : 200;
+ const body = error ? error.body : response;
+
+ server.respondWith('GET', `${API_BASE_PATH}/snapshot_policies`, [
+ status,
+ { 'Content-Type': 'application/json' },
+ JSON.stringify(body),
+ ]);
+ };
+
return {
setLoadPolicies,
+ setLoadSnapshotPolicies,
};
};
diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts
index 3cff2e3ab050f..7b227f822fa97 100644
--- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts
+++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/helpers/index.ts
@@ -4,4 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
-export type TestSubjects = 'waitForSnapshotField' | 'savePolicyButton';
+export type TestSubjects =
+ | 'snapshotPolicyCombobox'
+ | 'savePolicyButton'
+ | 'customPolicyCallout'
+ | 'noPoliciesCallout'
+ | 'policiesErrorCallout';
diff --git a/x-pack/plugins/index_lifecycle_management/kibana.json b/x-pack/plugins/index_lifecycle_management/kibana.json
index 6385646b95789..1a9f133b846fb 100644
--- a/x-pack/plugins/index_lifecycle_management/kibana.json
+++ b/x-pack/plugins/index_lifecycle_management/kibana.json
@@ -12,5 +12,10 @@
"usageCollection",
"indexManagement"
],
- "configPath": ["xpack", "ilm"]
+ "configPath": ["xpack", "ilm"],
+ "requiredBundles": [
+ "indexManagement",
+ "kibanaReact",
+ "esUiShared"
+ ]
}
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.js
index 299bf28778ab4..34d1c0f8de216 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.js
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/delete_phase/delete_phase.js
@@ -7,17 +7,12 @@
import React, { PureComponent, Fragment } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@kbn/i18n/react';
-import {
- EuiDescribedFormGroup,
- EuiSwitch,
- EuiFieldText,
- EuiTextColor,
- EuiFormRow,
-} from '@elastic/eui';
+import { EuiDescribedFormGroup, EuiSwitch, EuiTextColor, EuiFormRow } from '@elastic/eui';
import { PHASE_DELETE, PHASE_ENABLED, PHASE_WAIT_FOR_SNAPSHOT_POLICY } from '../../../../constants';
import { ActiveBadge, LearnMoreLink, OptionalLabel, PhaseErrorMessage } from '../../../components';
import { MinAgeInput } from '../min_age_input';
+import { SnapshotPolicies } from '../snapshot_policies';
export class DeletePhase extends PureComponent {
static propTypes = {
@@ -125,10 +120,9 @@ export class DeletePhase extends PureComponent {
}
>
- setPhaseData(PHASE_WAIT_FOR_SNAPSHOT_POLICY, e.target.value)}
+ onChange={(value) => setPhaseData(PHASE_WAIT_FOR_SNAPSHOT_POLICY, value)}
/>
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/index.ts
new file mode 100644
index 0000000000000..f33ce81eb6157
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+export { SnapshotPolicies } from './snapshot_policies';
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/snapshot_policies.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/snapshot_policies.tsx
new file mode 100644
index 0000000000000..76eae0f906d0c
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/snapshot_policies/snapshot_policies.tsx
@@ -0,0 +1,157 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React, { Fragment } from 'react';
+
+import { FormattedMessage } from '@kbn/i18n/react';
+import { i18n } from '@kbn/i18n';
+
+import {
+ EuiButtonIcon,
+ EuiCallOut,
+ EuiComboBox,
+ EuiComboBoxOptionOption,
+ EuiSpacer,
+} from '@elastic/eui';
+
+import { useLoadSnapshotPolicies } from '../../../../services/api';
+
+interface Props {
+ value: string;
+ onChange: (value: string) => void;
+}
+export const SnapshotPolicies: React.FunctionComponent = ({ value, onChange }) => {
+ const { error, isLoading, data, sendRequest } = useLoadSnapshotPolicies();
+
+ const policies = data.map((name: string) => ({
+ label: name,
+ value: name,
+ }));
+
+ const onComboChange = (options: EuiComboBoxOptionOption[]) => {
+ if (options.length > 0) {
+ onChange(options[0].label);
+ } else {
+ onChange('');
+ }
+ };
+
+ const onCreateOption = (newValue: string) => {
+ onChange(newValue);
+ };
+
+ let calloutContent;
+ if (error) {
+ calloutContent = (
+
+
+
+
+
+
+
+ }
+ >
+
+
+
+ );
+ } else if (data.length === 0) {
+ calloutContent = (
+
+
+
+ }
+ >
+
+
+
+ );
+ } else if (value && !data.includes(value)) {
+ calloutContent = (
+
+
+
+ }
+ >
+
+
+
+ );
+ }
+
+ return (
+
+
+ {calloutContent}
+
+ );
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js
index dad259681eb7a..500ab44d96694 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js
+++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/policy_table/components/policy_table/policy_table.js
@@ -254,7 +254,7 @@ export class PolicyTable extends Component {
icon: 'list',
onClick: () => {
this.props.navigateToApp('management', {
- path: `/data/index_management${getIndexListUri(`ilm.policy:${policy.name}`)}`,
+ path: `/data/index_management${getIndexListUri(`ilm.policy:${policy.name}`, true)}`,
});
},
});
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/api.js b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts
similarity index 56%
rename from x-pack/plugins/index_lifecycle_management/public/application/services/api.js
rename to x-pack/plugins/index_lifecycle_management/public/application/services/api.ts
index 6b46d6e6ea735..065fb3bcebca7 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/services/api.js
+++ b/x-pack/plugins/index_lifecycle_management/public/application/services/api.ts
@@ -4,6 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { METRIC_TYPE } from '@kbn/analytics';
+import { trackUiMetric } from './ui_metric';
+
import {
UIM_POLICY_DELETE,
UIM_POLICY_ATTACH_INDEX,
@@ -12,14 +15,13 @@ import {
UIM_INDEX_RETRY_STEP,
} from '../constants';
-import { trackUiMetric } from './ui_metric';
-import { sendGet, sendPost, sendDelete } from './http';
+import { sendGet, sendPost, sendDelete, useRequest } from './http';
export async function loadNodes() {
return await sendGet(`nodes/list`);
}
-export async function loadNodeDetails(selectedNodeAttrs) {
+export async function loadNodeDetails(selectedNodeAttrs: string) {
return await sendGet(`nodes/${selectedNodeAttrs}/details`);
}
@@ -27,45 +29,53 @@ export async function loadIndexTemplates() {
return await sendGet(`templates`);
}
-export async function loadPolicies(withIndices) {
+export async function loadPolicies(withIndices: boolean) {
return await sendGet('policies', { withIndices });
}
-export async function savePolicy(policy) {
+export async function savePolicy(policy: any) {
return await sendPost(`policies`, policy);
}
-export async function deletePolicy(policyName) {
+export async function deletePolicy(policyName: string) {
const response = await sendDelete(`policies/${encodeURIComponent(policyName)}`);
// Only track successful actions.
- trackUiMetric('count', UIM_POLICY_DELETE);
+ trackUiMetric(METRIC_TYPE.COUNT, UIM_POLICY_DELETE);
return response;
}
-export const retryLifecycleForIndex = async (indexNames) => {
+export const retryLifecycleForIndex = async (indexNames: string[]) => {
const response = await sendPost(`index/retry`, { indexNames });
// Only track successful actions.
- trackUiMetric('count', UIM_INDEX_RETRY_STEP);
+ trackUiMetric(METRIC_TYPE.COUNT, UIM_INDEX_RETRY_STEP);
return response;
};
-export const removeLifecycleForIndex = async (indexNames) => {
+export const removeLifecycleForIndex = async (indexNames: string[]) => {
const response = await sendPost(`index/remove`, { indexNames });
// Only track successful actions.
- trackUiMetric('count', UIM_POLICY_DETACH_INDEX);
+ trackUiMetric(METRIC_TYPE.COUNT, UIM_POLICY_DETACH_INDEX);
return response;
};
-export const addLifecyclePolicyToIndex = async (body) => {
+export const addLifecyclePolicyToIndex = async (body: any) => {
const response = await sendPost(`index/add`, body);
// Only track successful actions.
- trackUiMetric('count', UIM_POLICY_ATTACH_INDEX);
+ trackUiMetric(METRIC_TYPE.COUNT, UIM_POLICY_ATTACH_INDEX);
return response;
};
-export const addLifecyclePolicyToTemplate = async (body) => {
+export const addLifecyclePolicyToTemplate = async (body: any) => {
const response = await sendPost(`template`, body);
// Only track successful actions.
- trackUiMetric('count', UIM_POLICY_ATTACH_INDEX_TEMPLATE);
+ trackUiMetric(METRIC_TYPE.COUNT, UIM_POLICY_ATTACH_INDEX_TEMPLATE);
return response;
};
+
+export const useLoadSnapshotPolicies = () => {
+ return useRequest({
+ path: `snapshot_policies`,
+ method: 'get',
+ initialData: [],
+ });
+};
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/services/http.ts b/x-pack/plugins/index_lifecycle_management/public/application/services/http.ts
index 47e96ea28bb8c..c54ee15fd69bf 100644
--- a/x-pack/plugins/index_lifecycle_management/public/application/services/http.ts
+++ b/x-pack/plugins/index_lifecycle_management/public/application/services/http.ts
@@ -4,6 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import {
+ UseRequestConfig,
+ useRequest as _useRequest,
+ Error,
+} from '../../../../../../src/plugins/es_ui_shared/public';
+
let _httpClient: any;
export function init(httpClient: any): void {
@@ -24,10 +30,14 @@ export function sendPost(path: string, payload: any): any {
return _httpClient.post(getFullPath(path), { body: JSON.stringify(payload) });
}
-export function sendGet(path: string, query: any): any {
+export function sendGet(path: string, query?: any): any {
return _httpClient.get(getFullPath(path), { query });
}
export function sendDelete(path: string): any {
return _httpClient.delete(getFullPath(path));
}
+
+export const useRequest = (config: UseRequestConfig) => {
+ return _useRequest(_httpClient, { ...config, path: getFullPath(config.path) });
+};
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/index.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/index.ts
new file mode 100644
index 0000000000000..19fbc45010ea2
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { RouteDependencies } from '../../../types';
+import { registerFetchRoute } from './register_fetch_route';
+
+export function registerSnapshotPoliciesRoutes(dependencies: RouteDependencies) {
+ registerFetchRoute(dependencies);
+}
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/register_fetch_route.ts b/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/register_fetch_route.ts
new file mode 100644
index 0000000000000..7a52648e29ee8
--- /dev/null
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/api/snapshot_policies/register_fetch_route.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { LegacyAPICaller } from 'src/core/server';
+
+import { RouteDependencies } from '../../../types';
+import { addBasePath } from '../../../services';
+
+async function fetchSnapshotPolicies(callAsCurrentUser: LegacyAPICaller): Promise {
+ const params = {
+ method: 'GET',
+ path: '/_slm/policy',
+ };
+
+ return await callAsCurrentUser('transport.request', params);
+}
+
+export function registerFetchRoute({ router, license, lib }: RouteDependencies) {
+ router.get(
+ { path: addBasePath('/snapshot_policies'), validate: false },
+ license.guardApiRoute(async (context, request, response) => {
+ try {
+ const policiesByName = await fetchSnapshotPolicies(
+ context.core.elasticsearch.legacy.client.callAsCurrentUser
+ );
+ return response.ok({ body: Object.keys(policiesByName) });
+ } catch (e) {
+ if (lib.isEsError(e)) {
+ return response.customError({
+ statusCode: e.statusCode,
+ body: e,
+ });
+ }
+ // Case: default
+ return response.internalError({ body: e });
+ }
+ })
+ );
+}
diff --git a/x-pack/plugins/index_lifecycle_management/server/routes/index.ts b/x-pack/plugins/index_lifecycle_management/server/routes/index.ts
index 35996721854c6..f7390debbe177 100644
--- a/x-pack/plugins/index_lifecycle_management/server/routes/index.ts
+++ b/x-pack/plugins/index_lifecycle_management/server/routes/index.ts
@@ -10,10 +10,12 @@ import { registerIndexRoutes } from './api/index';
import { registerNodesRoutes } from './api/nodes';
import { registerPoliciesRoutes } from './api/policies';
import { registerTemplatesRoutes } from './api/templates';
+import { registerSnapshotPoliciesRoutes } from './api/snapshot_policies';
export function registerApiRoutes(dependencies: RouteDependencies) {
registerIndexRoutes(dependencies);
registerNodesRoutes(dependencies);
registerPoliciesRoutes(dependencies);
registerTemplatesRoutes(dependencies);
+ registerSnapshotPoliciesRoutes(dependencies);
}
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx
index d85db94d4a970..ad445f75f047c 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx
+++ b/x-pack/plugins/index_management/__jest__/client_integration/helpers/setup_environment.tsx
@@ -34,7 +34,11 @@ export const services = {
services.uiMetricService.setup({ reportUiStats() {} } as any);
setExtensionsService(services.extensionsService);
setUiMetricService(services.uiMetricService);
-const appDependencies = { services, core: { getUrlForApp: () => {} }, plugins: {} } as any;
+const appDependencies = {
+ services,
+ core: { getUrlForApp: () => {} },
+ plugins: {},
+} as any;
export const setupEnvironment = () => {
// Mock initialization of services
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts
index ecea230ecab85..9397ce21ba827 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.helpers.ts
@@ -166,7 +166,7 @@ export const setup = async (overridingDependencies: any = {}): Promise ({
name,
- timeStampField: { name: '@timestamp', mapping: { type: 'date' } },
+ timeStampField: { name: '@timestamp' },
indices: [
{
name: 'indexName',
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts
index dfcbb51869466..89a95135bb07a 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/home/data_streams_tab.test.ts
@@ -127,8 +127,8 @@ describe('Data Streams tab', () => {
const { tableCellsValues } = table.getMetaData('dataStreamTable');
expect(tableCellsValues).toEqual([
- ['', 'dataStream1', '1', ''],
- ['', 'dataStream2', '1', ''],
+ ['', 'dataStream1', '1', 'Delete'],
+ ['', 'dataStream2', '1', 'Delete'],
]);
});
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts
index 0047e4c0294cb..a397419053351 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.helpers.ts
@@ -95,9 +95,9 @@ const createActions = (testBed: TestBed) => {
find('closeDetailsButton').simulate('click');
};
- const toggleViewItem = (view: 'composable' | 'system') => {
+ const toggleViewItem = (view: 'managed' | 'cloudManaged' | 'system') => {
const { find, component } = testBed;
- const views = ['composable', 'system'];
+ const views = ['managed', 'cloudManaged', 'system'];
// First open the pop over
act(() => {
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts
index 1ec29f1c5b894..f7ebc0bcf632b 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts
+++ b/x-pack/plugins/index_management/__jest__/client_integration/home/index_templates_tab.test.ts
@@ -63,7 +63,6 @@ describe('Index Templates tab', () => {
},
},
});
- (template1 as any).hasSettings = true;
const template2 = fixtures.getTemplate({
name: `b${getRandomString()}`,
@@ -73,6 +72,7 @@ describe('Index Templates tab', () => {
const template3 = fixtures.getTemplate({
name: `.c${getRandomString()}`, // mock system template
indexPatterns: ['template3Pattern1*', 'template3Pattern2', 'template3Pattern3'],
+ type: 'system',
});
const template4 = fixtures.getTemplate({
@@ -101,6 +101,7 @@ describe('Index Templates tab', () => {
name: `.c${getRandomString()}`, // mock system template
indexPatterns: ['template6Pattern1*', 'template6Pattern2', 'template6Pattern3'],
isLegacy: true,
+ type: 'system',
});
const templates = [template1, template2, template3];
@@ -124,44 +125,49 @@ describe('Index Templates tab', () => {
// Test composable table content
tableCellsValues.forEach((row, i) => {
const indexTemplate = templates[i];
- const { name, indexPatterns, priority, ilmPolicy, composedOf, template } = indexTemplate;
+ const { name, indexPatterns, ilmPolicy, composedOf, template } = indexTemplate;
const hasContent = !!template.settings || !!template.mappings || !!template.aliases;
const ilmPolicyName = ilmPolicy && ilmPolicy.name ? ilmPolicy.name : '';
const composedOfString = composedOf ? composedOf.join(',') : '';
- const priorityFormatted = priority ? priority.toString() : '';
-
- expect(removeWhiteSpaceOnArrayValues(row)).toEqual([
- '', // Checkbox to select row
- name,
- indexPatterns.join(', '),
- ilmPolicyName,
- composedOfString,
- priorityFormatted,
- hasContent ? 'M S A' : 'None', // M S A -> Mappings Settings Aliases badges
- '', // Column of actions
- ]);
+
+ try {
+ expect(removeWhiteSpaceOnArrayValues(row)).toEqual([
+ '', // Checkbox to select row
+ name,
+ indexPatterns.join(', '),
+ ilmPolicyName,
+ composedOfString,
+ hasContent ? 'M S A' : 'None', // M S A -> Mappings Settings Aliases badges
+ 'EditDelete', // Column of actions
+ ]);
+ } catch (e) {
+ console.error(`Error in index template at row ${i}`); // eslint-disable-line no-console
+ throw e;
+ }
});
// Test legacy table content
legacyTableCellsValues.forEach((row, i) => {
- const template = legacyTemplates[i];
- const { name, indexPatterns, order, ilmPolicy } = template;
+ const legacyIndexTemplate = legacyTemplates[i];
+ const { name, indexPatterns, ilmPolicy, template } = legacyIndexTemplate;
+ const hasContent = !!template.settings || !!template.mappings || !!template.aliases;
const ilmPolicyName = ilmPolicy && ilmPolicy.name ? ilmPolicy.name : '';
- const orderFormatted = order ? order.toString() : order;
-
- expect(removeWhiteSpaceOnArrayValues(row)).toEqual([
- '',
- name,
- indexPatterns.join(', '),
- ilmPolicyName,
- orderFormatted,
- '',
- '',
- '',
- '',
- ]);
+
+ try {
+ expect(removeWhiteSpaceOnArrayValues(row)).toEqual([
+ '',
+ name,
+ indexPatterns.join(', '),
+ ilmPolicyName,
+ hasContent ? 'M S A' : 'None', // M S A -> Mappings Settings Aliases badges
+ 'EditDelete', // Column of actions
+ ]);
+ } catch (e) {
+ console.error(`Error in legacy template at row ${i}`); // eslint-disable-line no-console
+ throw e;
+ }
});
});
@@ -211,7 +217,7 @@ describe('Index Templates tab', () => {
await actions.clickTemplateAt(0);
expect(exists('templateList')).toBe(true);
expect(exists('templateDetails')).toBe(true);
- expect(find('templateDetails.title').text()).toBe(templates[0].name);
+ expect(find('templateDetails.title').text().trim()).toBe(templates[0].name);
// Close flyout
await act(async () => {
@@ -223,7 +229,7 @@ describe('Index Templates tab', () => {
expect(exists('templateList')).toBe(true);
expect(exists('templateDetails')).toBe(true);
- expect(find('templateDetails.title').text()).toBe(legacyTemplates[0].name);
+ expect(find('templateDetails.title').text().trim()).toBe(legacyTemplates[0].name);
});
describe('table row actions', () => {
@@ -460,7 +466,7 @@ describe('Index Templates tab', () => {
const { find } = testBed;
const [{ name }] = templates;
- expect(find('templateDetails.title').text()).toEqual(name);
+ expect(find('templateDetails.title').text().trim()).toEqual(name);
});
it('should have a close button and be able to close flyout', async () => {
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx
index 69d7a13edfcfb..76b6c34f999d5 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx
+++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_create.test.tsx
@@ -368,8 +368,8 @@ describe.skip(' ', () => {
aliases: ALIASES,
},
_kbnMeta: {
+ type: 'default',
isLegacy: false,
- isManaged: false,
},
};
diff --git a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx
index 9f0e81454f0af..de66013241236 100644
--- a/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx
+++ b/x-pack/plugins/index_management/__jest__/client_integration/index_template_wizard/template_edit.test.tsx
@@ -213,7 +213,7 @@ describe.skip(' ', () => {
aliases: ALIASES,
},
_kbnMeta: {
- isManaged: false,
+ type: 'default',
isLegacy: templateToEdit._kbnMeta.isLegacy,
},
};
diff --git a/x-pack/plugins/index_management/__jest__/components/index_table.test.js b/x-pack/plugins/index_management/__jest__/components/index_table.test.js
index 8e8c2632a2372..49902d8b09675 100644
--- a/x-pack/plugins/index_management/__jest__/components/index_table.test.js
+++ b/x-pack/plugins/index_management/__jest__/components/index_table.test.js
@@ -198,18 +198,10 @@ describe('index table', () => {
});
test('should show system indices only when the switch is turned on', () => {
const rendered = mountWithIntl(component);
- snapshot(
- rendered
- .find('.euiPagination .euiPaginationButton .euiButtonEmpty__content > span')
- .map((span) => span.text())
- );
+ snapshot(rendered.find('.euiPagination li').map((item) => item.text()));
const switchControl = rendered.find('.euiSwitch__button');
switchControl.simulate('click');
- snapshot(
- rendered
- .find('.euiPagination .euiPaginationButton .euiButtonEmpty__content > span')
- .map((span) => span.text())
- );
+ snapshot(rendered.find('.euiPagination li').map((item) => item.text()));
});
test('should filter based on content of search input', () => {
const rendered = mountWithIntl(component);
diff --git a/x-pack/plugins/index_management/common/lib/template_serialization.ts b/x-pack/plugins/index_management/common/lib/template_serialization.ts
index 5c55860bda81b..069d6ac29fbca 100644
--- a/x-pack/plugins/index_management/common/lib/template_serialization.ts
+++ b/x-pack/plugins/index_management/common/lib/template_serialization.ts
@@ -8,18 +8,28 @@ import {
LegacyTemplateSerialized,
TemplateSerialized,
TemplateListItem,
+ TemplateType,
} from '../types';
const hasEntries = (data: object = {}) => Object.entries(data).length > 0;
export function serializeTemplate(templateDeserialized: TemplateDeserialized): TemplateSerialized {
- const { version, priority, indexPatterns, template, composedOf, _meta } = templateDeserialized;
+ const {
+ version,
+ priority,
+ indexPatterns,
+ template,
+ composedOf,
+ dataStream,
+ _meta,
+ } = templateDeserialized;
return {
version,
priority,
template,
index_patterns: indexPatterns,
+ data_stream: dataStream,
composed_of: composedOf,
_meta,
};
@@ -41,6 +51,15 @@ export function deserializeTemplate(
} = templateEs;
const { settings } = template;
+ let type: TemplateType = 'default';
+ if (Boolean(cloudManagedTemplatePrefix && name.startsWith(cloudManagedTemplatePrefix))) {
+ type = 'cloudManaged';
+ } else if (name.startsWith('.')) {
+ type = 'system';
+ } else if (Boolean(_meta?.managed === true)) {
+ type = 'managed';
+ }
+
const deserializedTemplate: TemplateDeserialized = {
name,
version,
@@ -52,10 +71,7 @@ export function deserializeTemplate(
dataStream,
_meta,
_kbnMeta: {
- isManaged: Boolean(_meta?.managed === true),
- isCloudManaged: Boolean(
- cloudManagedTemplatePrefix && name.startsWith(cloudManagedTemplatePrefix)
- ),
+ type,
hasDatastream: Boolean(dataStream),
},
};
diff --git a/x-pack/plugins/index_management/common/lib/utils.ts b/x-pack/plugins/index_management/common/lib/utils.ts
index 5a7db8ef50ab4..1dc6f4a486a2c 100644
--- a/x-pack/plugins/index_management/common/lib/utils.ts
+++ b/x-pack/plugins/index_management/common/lib/utils.ts
@@ -23,5 +23,5 @@ export const getTemplateParameter = (
) => {
return isLegacyTemplate(template)
? (template as LegacyTemplateSerialized)[setting]
- : (template as TemplateSerialized).template[setting];
+ : (template as TemplateSerialized).template?.[setting];
};
diff --git a/x-pack/plugins/index_management/common/types/data_streams.ts b/x-pack/plugins/index_management/common/types/data_streams.ts
index 772ed43459bcf..d1936c4426b49 100644
--- a/x-pack/plugins/index_management/common/types/data_streams.ts
+++ b/x-pack/plugins/index_management/common/types/data_streams.ts
@@ -6,9 +6,6 @@
interface TimestampFieldFromEs {
name: string;
- mapping: {
- type: string;
- };
}
type TimestampField = TimestampFieldFromEs;
diff --git a/x-pack/plugins/index_management/common/types/templates.ts b/x-pack/plugins/index_management/common/types/templates.ts
index fdcac40ca596f..32e254e490b2a 100644
--- a/x-pack/plugins/index_management/common/types/templates.ts
+++ b/x-pack/plugins/index_management/common/types/templates.ts
@@ -38,23 +38,24 @@ export interface TemplateDeserialized {
aliases?: Aliases;
mappings?: Mappings;
};
- composedOf?: string[]; // Used on composable index template
+ composedOf?: string[]; // Composable template only
version?: number;
- priority?: number;
- order?: number; // Used on legacy index template
+ priority?: number; // Composable template only
+ order?: number; // Legacy template only
ilmPolicy?: {
name: string;
};
- _meta?: { [key: string]: any };
- dataStream?: { timestamp_field: string };
+ _meta?: { [key: string]: any }; // Composable template only
+ dataStream?: { timestamp_field: string }; // Composable template only
_kbnMeta: {
- isManaged: boolean;
- isCloudManaged: boolean;
+ type: TemplateType;
hasDatastream: boolean;
isLegacy?: boolean;
};
}
+export type TemplateType = 'default' | 'managed' | 'cloudManaged' | 'system';
+
export interface TemplateFromEs {
name: string;
index_template: TemplateSerialized;
@@ -78,8 +79,7 @@ export interface TemplateListItem {
name: string;
};
_kbnMeta: {
- isManaged: boolean;
- isCloudManaged: boolean;
+ type: TemplateType;
hasDatastream: boolean;
isLegacy?: boolean;
};
diff --git a/x-pack/plugins/index_management/kibana.json b/x-pack/plugins/index_management/kibana.json
index 40ecb26e8f0c9..6ab691054382e 100644
--- a/x-pack/plugins/index_management/kibana.json
+++ b/x-pack/plugins/index_management/kibana.json
@@ -13,5 +13,9 @@
"usageCollection",
"ingestManager"
],
- "configPath": ["xpack", "index_management"]
+ "configPath": ["xpack", "index_management"],
+ "requiredBundles": [
+ "kibanaReact",
+ "esUiShared"
+ ]
}
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx
index 6c8da4684f019..75eb419d56a5c 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_create.test.tsx
@@ -177,8 +177,6 @@ describe(' ', () => {
template: {
settings: SETTINGS,
mappings: {
- _source: {},
- _meta: {},
properties: {
[BOOLEAN_MAPPING_FIELD.name]: {
type: BOOLEAN_MAPPING_FIELD.type,
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx
index f237605756d5c..115fdf032da8f 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_edit.test.tsx
@@ -109,11 +109,6 @@ describe(' ', () => {
...COMPONENT_TEMPLATE_TO_EDIT,
template: {
...COMPONENT_TEMPLATE_TO_EDIT.template,
- mappings: {
- _meta: {},
- _source: {},
- properties: {},
- },
},
};
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts
index 86eb88017b77f..6f09e51255f3b 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/__jest__/client_integration/component_template_list.test.ts
@@ -65,7 +65,7 @@ describe(' ', () => {
const { name, usedBy } = componentTemplates[i];
const usedByText = usedBy.length === 0 ? 'Not in use' : usedBy.length.toString();
- expect(row).toEqual(['', name, usedByText, '', '', '', '']);
+ expect(row).toEqual(['', name, usedByText, '', '', '', 'EditDelete']);
});
});
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates.tsx
index 64c7cd400ba0d..ea5632ac86192 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates.tsx
@@ -26,8 +26,15 @@ interface Filters {
[key: string]: { name: string; checked: 'on' | 'off' };
}
+/**
+ * Copied from https://stackoverflow.com/a/9310752
+ */
+function escapeRegExp(text: string) {
+ return text.replace(/[-\[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
+}
+
function fuzzyMatch(searchValue: string, text: string) {
- const pattern = `.*${searchValue.split('').join('.*')}.*`;
+ const pattern = `.*${searchValue.split('').map(escapeRegExp).join('.*')}.*`;
const regex = new RegExp(pattern);
return regex.test(text);
}
@@ -48,7 +55,7 @@ const i18nTexts = {
searchBoxPlaceholder: i18n.translate(
'xpack.idxMgmt.componentTemplatesSelector.searchBox.placeholder',
{
- defaultMessage: 'Search components',
+ defaultMessage: 'Search component templates',
}
),
};
@@ -78,24 +85,33 @@ export const ComponentTemplates = ({ isLoading, components, listItemProps }: Pro
return [];
}
- return components.filter((component) => {
- if (filters.settings.checked === 'on' && !component.hasSettings) {
- return false;
- }
- if (filters.mappings.checked === 'on' && !component.hasMappings) {
- return false;
- }
- if (filters.aliases.checked === 'on' && !component.hasAliases) {
- return false;
- }
-
- if (searchValue.trim() === '') {
- return true;
- }
-
- const match = fuzzyMatch(searchValue, component.name);
- return match;
- });
+ return components
+ .filter((component) => {
+ if (filters.settings.checked === 'on' && !component.hasSettings) {
+ return false;
+ }
+ if (filters.mappings.checked === 'on' && !component.hasMappings) {
+ return false;
+ }
+ if (filters.aliases.checked === 'on' && !component.hasAliases) {
+ return false;
+ }
+
+ if (searchValue.trim() === '') {
+ return true;
+ }
+
+ const match = fuzzyMatch(searchValue, component.name);
+ return match;
+ })
+ .sort((a, b) => {
+ if (a.name < b.name) {
+ return -1;
+ } else if (a.name > b.name) {
+ return 1;
+ }
+ return 0;
+ });
}, [isLoading, components, searchValue, filters]);
const isSearchResultEmpty = filteredComponents.length === 0 && components.length > 0;
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.scss b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.scss
index 6abbbe65790e7..61d5512da2cd9 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.scss
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.scss
@@ -32,5 +32,9 @@
font-weight: 600;
}
}
+
+ &__content {
+ mask-image: none;
+ }
}
}
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx
index af48c3c79379a..8795c08fd2bee 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_selector/component_templates_selector.tsx
@@ -96,7 +96,7 @@ export const ComponentTemplatesSelector = ({
);
@@ -136,7 +136,7 @@ export const ComponentTemplatesSelector = ({
}}
/>
-
+
)}
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx
index 8762eae9d2297..18988fa125a06 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics.tsx
@@ -200,7 +200,7 @@ export const StepLogistics: React.FunctionComponent
= React.memo(
>
}
>
- {isMetaVisible ? (
+ {isMetaVisible && (
= React.memo(
'aria-label': i18n.translate(
'xpack.idxMgmt.componentTemplateForm.stepLogistics.metaAriaLabel',
{
- defaultMessage: 'Metadata JSON editor',
+ defaultMessage: '_meta field data editor',
}
),
},
}}
/>
- ) : (
- // requires children or a field
- // For now, we return an empty if the editor is not visible
-
)}
diff --git a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_schema.tsx b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_schema.tsx
index 0c52037abde45..c577957339487 100644
--- a/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_schema.tsx
+++ b/x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_form/steps/step_logistics_schema.tsx
@@ -65,7 +65,7 @@ export const logisticsFormSchema: FormSchema = {
},
_meta: {
label: i18n.translate('xpack.idxMgmt.componentTemplateForm.stepLogistics.metaFieldLabel', {
- defaultMessage: 'Metadata (optional)',
+ defaultMessage: '_meta field data (optional)',
}),
helpText: (
- Object.keys(json).length ? JSON.stringify(json, null, 2) : '{\n\n}';
-
const formSerializer: SerializerFunc = (formData) => {
const {
dynamicMapping: {
@@ -40,22 +37,17 @@ const formSerializer: SerializerFunc = (formData) => {
const dynamic = dynamicMappingsEnabled ? true : throwErrorsForUnmappedFields ? 'strict' : false;
- let parsedMeta;
- try {
- parsedMeta = JSON.parse(metaField);
- } catch {
- parsedMeta = {};
- }
-
- return {
+ const serialized = {
dynamic,
numeric_detection,
date_detection,
dynamic_date_formats,
- _source: { ...sourceField },
- _meta: parsedMeta,
+ _source: sourceField,
+ _meta: metaField,
_routing,
};
+
+ return serialized;
};
const formDeserializer = (formData: GenericObject) => {
@@ -64,7 +56,11 @@ const formDeserializer = (formData: GenericObject) => {
numeric_detection,
date_detection,
dynamic_date_formats,
- _source: { enabled, includes, excludes },
+ _source: { enabled, includes, excludes } = {} as {
+ enabled?: boolean;
+ includes?: string[];
+ excludes?: string[];
+ },
_meta,
_routing,
} = formData;
@@ -82,7 +78,7 @@ const formDeserializer = (formData: GenericObject) => {
includes,
excludes,
},
- metaField: stringifyJson(_meta),
+ metaField: _meta ?? {},
_routing,
};
};
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx
index c06340fd9ae14..6e80f8b813ec2 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/configuration_form/configuration_form_schema.tsx
@@ -48,10 +48,30 @@ export const configurationFormSchema: FormSchema = {
validator: isJsonField(
i18n.translate('xpack.idxMgmt.mappingsEditor.configuration.metaFieldEditorJsonError', {
defaultMessage: 'The _meta field JSON is not valid.',
- })
+ }),
+ { allowEmptyString: true }
),
},
],
+ deserializer: (value: any) => {
+ if (value === '') {
+ return value;
+ }
+ return JSON.stringify(value, null, 2);
+ },
+ serializer: (value: string) => {
+ try {
+ const parsed = JSON.parse(value);
+ // If an empty object was passed, strip out this value entirely.
+ if (!Object.keys(parsed).length) {
+ return undefined;
+ }
+ return parsed;
+ } catch (error) {
+ // swallow error and return non-parsed value;
+ return value;
+ }
+ },
},
sourceField: {
enabled: {
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx
index 80937e7da1192..79685d46b6bdd 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/templates_form/templates_form.tsx
@@ -22,7 +22,7 @@ interface Props {
const stringifyJson = (json: { [key: string]: any }) =>
Array.isArray(json) ? JSON.stringify(json, null, 2) : '[\n\n]';
-const formSerializer: SerializerFunc = (formData) => {
+const formSerializer: SerializerFunc = (formData) => {
const { dynamicTemplates } = formData;
let parsedTemplates;
@@ -34,12 +34,14 @@ const formSerializer: SerializerFunc = (formData) => {
parsedTemplates = [parsedTemplates];
}
} catch {
- parsedTemplates = [];
+ // Silently swallow errors
}
- return {
- dynamic_templates: parsedTemplates,
- };
+ return Array.isArray(parsedTemplates) && parsedTemplates.length > 0
+ ? {
+ dynamic_templates: parsedTemplates,
+ }
+ : undefined;
};
const formDeserializer = (formData: { [key: string]: any }) => {
@@ -53,7 +55,7 @@ const formDeserializer = (formData: { [key: string]: any }) => {
export const TemplatesForm = React.memo(({ value }: Props) => {
const isMounted = useRef(undefined);
- const { form } = useForm({
+ const { form } = useForm({
schema: templatesFormSchema,
serializer: formSerializer,
deserializer: formDeserializer,
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts
index 9fa4a7981c047..8b3ff60005305 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/lib/utils.ts
@@ -199,7 +199,7 @@ export const getTypeMetaFromSource = (
*
* @param fieldsToNormalize The "properties" object from the mappings (or "fields" object for `text` and `keyword` types)
*/
-export const normalize = (fieldsToNormalize: Fields): NormalizedFields => {
+export const normalize = (fieldsToNormalize: Fields = {}): NormalizedFields => {
let maxNestedDepth = 0;
const normalizeFields = (
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx
index 46dc1176f62b4..e8fda90737708 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_editor.tsx
@@ -39,14 +39,14 @@ export const MappingsEditor = React.memo(({ onChange, value, indexSettings }: Pr
}
const {
- _source = {},
- _meta = {},
+ _source,
+ _meta,
_routing,
dynamic,
numeric_detection,
date_detection,
dynamic_date_formats,
- properties = {},
+ properties,
dynamic_templates,
} = mappingsDefinition;
diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx
index fb4bfae974000..ad5056fa73ce1 100644
--- a/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx
+++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/mappings_state.tsx
@@ -19,7 +19,7 @@ import { normalize, deNormalize, stripUndefinedValues } from './lib';
type Mappings = MappingsTemplates &
MappingsConfiguration & {
- properties: MappingsFields;
+ properties?: MappingsFields;
};
export interface Types {
@@ -31,7 +31,7 @@ export interface Types {
export interface OnUpdateHandlerArg {
isValid?: boolean;
- getData: () => Mappings;
+ getData: () => Mappings | undefined;
validate: () => Promise;
}
@@ -114,13 +114,18 @@ export const MappingsState = React.memo(({ children, onChange, value }: Props) =
const configurationData = state.configuration.data.format();
const templatesData = state.templates.data.format();
- return {
+ const output = {
...stripUndefinedValues({
...configurationData,
...templatesData,
}),
- properties: fields,
};
+
+ if (fields && Object.keys(fields).length > 0) {
+ output.properties = fields;
+ }
+
+ return Object.keys(output).length > 0 ? (output as Mappings) : undefined;
},
validate: async () => {
const configurationFormValidator =
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components.tsx
index 01771f40f89ea..df0cc791384fe 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components.tsx
+++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_components.tsx
@@ -25,6 +25,12 @@ interface Props {
}
const i18nTexts = {
+ title: (
+
+ ),
description: (
{
- onChange({ isValid: true, validate: async () => true, getData: () => components });
+ onChange({
+ isValid: true,
+ validate: async () => true,
+ getData: () => (components.length > 0 ? components : undefined),
+ });
},
[onChange]
);
@@ -63,12 +73,7 @@ export const StepComponents = ({ defaultValue = [], onChange, esDocsBase }: Prop
-
-
-
+ {i18nTexts.title}
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx
index 44ec4db0873f3..ad98aee5fb5f1 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx
+++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_logistics.tsx
@@ -4,7 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useEffect } from 'react';
-import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiButtonEmpty, EuiSpacer } from '@elastic/eui';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiTitle,
+ EuiButtonEmpty,
+ EuiSpacer,
+ EuiLink,
+} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
@@ -16,6 +23,7 @@ import {
Field,
Forms,
JsonEditorField,
+ FormDataProvider,
} from '../../../../shared_imports';
import { documentationService } from '../../../services/documentation';
import { schemas, nameConfig, nameConfigWithoutValidations } from '../template_form_schemas';
@@ -24,70 +32,126 @@ import { schemas, nameConfig, nameConfigWithoutValidations } from '../template_f
const UseField = getUseField({ component: Field });
const FormRow = getFormRow({ titleTag: 'h3' });
-const fieldsMeta = {
- name: {
- title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.nameTitle', {
- defaultMessage: 'Name',
- }),
- description: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.nameDescription', {
- defaultMessage: 'A unique identifier for this template.',
- }),
- testSubject: 'nameField',
- },
- indexPatterns: {
- title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.indexPatternsTitle', {
- defaultMessage: 'Index patterns',
- }),
- description: i18n.translate(
- 'xpack.idxMgmt.templateForm.stepLogistics.indexPatternsDescription',
- {
- defaultMessage: 'The index patterns to apply to the template.',
- }
- ),
- testSubject: 'indexPatternsField',
- },
- order: {
- title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.orderTitle', {
- defaultMessage: 'Merge order',
- }),
- description: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.orderDescription', {
- defaultMessage: 'The merge order when multiple templates match an index.',
- }),
- testSubject: 'orderField',
- },
- priority: {
- title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.priorityTitle', {
- defaultMessage: 'Merge priority',
- }),
- description: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.priorityDescription', {
- defaultMessage: 'The merge priority when multiple templates match an index.',
- }),
- testSubject: 'priorityField',
- },
- version: {
- title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.versionTitle', {
- defaultMessage: 'Version',
- }),
- description: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.versionDescription', {
- defaultMessage: 'A number that identifies the template to external management systems.',
- }),
- testSubject: 'versionField',
- },
-};
+function getFieldsMeta(esDocsBase: string) {
+ return {
+ name: {
+ title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.nameTitle', {
+ defaultMessage: 'Name',
+ }),
+ description: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.nameDescription', {
+ defaultMessage: 'A unique identifier for this template.',
+ }),
+ testSubject: 'nameField',
+ },
+ indexPatterns: {
+ title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.indexPatternsTitle', {
+ defaultMessage: 'Index patterns',
+ }),
+ description: i18n.translate(
+ 'xpack.idxMgmt.templateForm.stepLogistics.indexPatternsDescription',
+ {
+ defaultMessage: 'The index patterns to apply to the template.',
+ }
+ ),
+ testSubject: 'indexPatternsField',
+ },
+ dataStream: {
+ title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.dataStreamTitle', {
+ defaultMessage: 'Data stream',
+ }),
+ description: (
+
+ {i18n.translate(
+ 'xpack.idxMgmt.templateForm.stepLogistics.dataStreamDocumentionLink',
+ {
+ defaultMessage: 'Learn more about data streams.',
+ }
+ )}
+
+ ),
+ }}
+ />
+ ),
+ testSubject: 'dataStreamField',
+ },
+ order: {
+ title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.orderTitle', {
+ defaultMessage: 'Merge order',
+ }),
+ description: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.orderDescription', {
+ defaultMessage: 'The merge order when multiple templates match an index.',
+ }),
+ testSubject: 'orderField',
+ },
+ priority: {
+ title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.priorityTitle', {
+ defaultMessage: 'Priority',
+ }),
+ description: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.priorityDescription', {
+ defaultMessage: 'Only the highest priority template will be applied.',
+ }),
+ testSubject: 'priorityField',
+ },
+ version: {
+ title: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.versionTitle', {
+ defaultMessage: 'Version',
+ }),
+ description: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.versionDescription', {
+ defaultMessage: 'A number that identifies the template to external management systems.',
+ }),
+ testSubject: 'versionField',
+ },
+ };
+}
+
+interface LogisticsForm {
+ [key: string]: any;
+}
+
+interface LogisticsFormInternal extends LogisticsForm {
+ __internal__: {
+ addMeta: boolean;
+ };
+}
interface Props {
- defaultValue: { [key: string]: any };
+ defaultValue: LogisticsForm;
onChange: (content: Forms.Content) => void;
isEditing?: boolean;
isLegacy?: boolean;
}
+function formDeserializer(formData: LogisticsForm): LogisticsFormInternal {
+ return {
+ ...formData,
+ __internal__: {
+ addMeta: Boolean(formData._meta && Object.keys(formData._meta).length),
+ },
+ };
+}
+
+function formSerializer(formData: LogisticsFormInternal): LogisticsForm {
+ const { __internal__, ...rest } = formData;
+ return rest;
+}
+
export const StepLogistics: React.FunctionComponent = React.memo(
({ defaultValue, isEditing = false, onChange, isLegacy = false }) => {
const { form } = useForm({
schema: schemas.logistics,
defaultValue,
options: { stripEmptyFields: false },
+ serializer: formSerializer,
+ deserializer: formDeserializer,
});
/**
@@ -117,7 +181,9 @@ export const StepLogistics: React.FunctionComponent = React.memo(
return subscription.unsubscribe;
}, [onChange]); // eslint-disable-line react-hooks/exhaustive-deps
- const { name, indexPatterns, order, priority, version } = fieldsMeta;
+ const { name, indexPatterns, dataStream, order, priority, version } = getFieldsMeta(
+ documentationService.getEsDocsBase()
+ );
return (
<>
@@ -180,6 +246,16 @@ export const StepLogistics: React.FunctionComponent = React.memo(
/>
+ {/* Create data stream */}
+ {isLegacy !== true && (
+
+
+
+ )}
+
{/* Order */}
{isLegacy && (
@@ -226,25 +302,35 @@ export const StepLogistics: React.FunctionComponent = React.memo(
id="xpack.idxMgmt.templateForm.stepLogistics.metaFieldDescription"
defaultMessage="Use the _meta field to store any metadata you want."
/>
+
+
>
}
>
-
+ {({ '__internal__.addMeta': addMeta }) => {
+ return (
+ addMeta && (
+
+ )
+ );
}}
- />
+
)}
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx
index 880c7fbd7f23c..0f4b9de4f6cfa 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx
+++ b/x-pack/plugins/index_management/public/application/components/template_form/steps/step_review.tsx
@@ -168,7 +168,7 @@ export const StepReview: React.FunctionComponent = React.memo(
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx
index 6310ac09488e5..f5c9be9292cd0 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx
+++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form.tsx
@@ -50,7 +50,7 @@ const wizardSections: { [id: string]: { id: WizardSection; label: string } } = {
components: {
id: 'components',
label: i18n.translate('xpack.idxMgmt.templateForm.steps.componentsStepName', {
- defaultMessage: 'Components',
+ defaultMessage: 'Component templates',
}),
},
settings: {
@@ -91,15 +91,9 @@ export const TemplateForm = ({
const indexTemplate = defaultValue ?? {
name: '',
indexPatterns: [],
- composedOf: [],
- template: {
- settings: {},
- mappings: {},
- aliases: {},
- },
+ template: {},
_kbnMeta: {
- isManaged: false,
- isCloudManaged: false,
+ type: 'default',
hasDatastream: false,
isLegacy,
},
@@ -150,18 +144,50 @@ export const TemplateForm = ({
>
) : null;
- const buildTemplateObject = (initialTemplate: TemplateDeserialized) => (
- wizardData: WizardContent
- ): TemplateDeserialized => ({
- ...initialTemplate,
- ...wizardData.logistics,
- composedOf: wizardData.components,
- template: {
- settings: wizardData.settings,
- mappings: wizardData.mappings,
- aliases: wizardData.aliases,
+ /**
+ * If no mappings, settings or aliases are defined, it is better to not send empty
+ * object for those values.
+ * This method takes care of that and other cleanup of empty fields.
+ * @param template The template object to clean up
+ */
+ const cleanupTemplateObject = (template: TemplateDeserialized) => {
+ const outputTemplate = { ...template };
+
+ if (outputTemplate.template.settings === undefined) {
+ delete outputTemplate.template.settings;
+ }
+ if (outputTemplate.template.mappings === undefined) {
+ delete outputTemplate.template.mappings;
+ }
+ if (outputTemplate.template.aliases === undefined) {
+ delete outputTemplate.template.aliases;
+ }
+ if (Object.keys(outputTemplate.template).length === 0) {
+ delete outputTemplate.template;
+ }
+
+ return outputTemplate;
+ };
+
+ const buildTemplateObject = useCallback(
+ (initialTemplate: TemplateDeserialized) => (
+ wizardData: WizardContent
+ ): TemplateDeserialized => {
+ const outputTemplate = {
+ ...initialTemplate,
+ ...wizardData.logistics,
+ composedOf: wizardData.components,
+ template: {
+ settings: wizardData.settings,
+ mappings: wizardData.mappings,
+ aliases: wizardData.aliases,
+ },
+ };
+
+ return cleanupTemplateObject(outputTemplate);
},
- });
+ []
+ );
const onSaveTemplate = useCallback(
async (wizardData: WizardContent) => {
@@ -177,7 +203,7 @@ export const TemplateForm = ({
clearSaveError();
},
- [indexTemplate, onSave, clearSaveError]
+ [indexTemplate, buildTemplateObject, onSave, clearSaveError]
);
return (
diff --git a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx
index 5af3b4dd00c4f..d8c3ad8c259fc 100644
--- a/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx
+++ b/x-pack/plugins/index_management/public/application/components/template_form/template_form_schemas.tsx
@@ -128,6 +128,32 @@ export const schemas: Record = {
},
],
},
+ dataStream: {
+ type: FIELD_TYPES.TOGGLE,
+ label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.datastreamLabel', {
+ defaultMessage: 'Create data stream',
+ }),
+ defaultValue: false,
+ serializer: (value) => {
+ if (value === true) {
+ return {
+ timestamp_field: '@timestamp',
+ };
+ }
+ },
+ deserializer: (value) => {
+ if (typeof value === 'boolean') {
+ return value;
+ }
+
+ /**
+ * For now, it is enough to have a "data_stream" declared on the index template
+ * to assume that the template creates a data stream. In the future, this condition
+ * might change
+ */
+ return value !== undefined;
+ },
+ },
order: {
type: FIELD_TYPES.NUMBER,
label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.fieldOrderLabel', {
@@ -187,5 +213,13 @@ export const schemas: Record = {
}
},
},
+ __internal__: {
+ addMeta: {
+ type: FIELD_TYPES.TOGGLE,
+ label: i18n.translate('xpack.idxMgmt.templateForm.stepLogistics.addMetadataLabel', {
+ defaultMessage: 'Add metadata',
+ }),
+ },
+ },
},
};
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 7bd04cdbf0c91..ee8970a3c4509 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
@@ -144,7 +144,6 @@ export const IndexManagementHome: React.FunctionComponent
-
index.name);
const selectedIndexNames = Object.keys(selectedIndicesMap);
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts
index 156d792c26f1d..3954ce04ca0b5 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/index.ts
@@ -5,3 +5,5 @@
*/
export * from './filter_list_button';
+
+export * from './template_type_indicator';
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/components/template_type_indicator.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/template_type_indicator.tsx
new file mode 100644
index 0000000000000..c6b0e21ebfdc1
--- /dev/null
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/components/template_type_indicator.tsx
@@ -0,0 +1,37 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { i18n } from '@kbn/i18n';
+import { EuiBadge } from '@elastic/eui';
+
+import { TemplateType } from '../../../../../../common';
+
+interface Props {
+ templateType: TemplateType;
+}
+
+const i18nTexts = {
+ managed: i18n.translate('xpack.idxMgmt.templateBadgeType.managed', {
+ defaultMessage: 'Managed',
+ }),
+ cloudManaged: i18n.translate('xpack.idxMgmt.templateBadgeType.cloudManaged', {
+ defaultMessage: 'Cloud-managed',
+ }),
+ system: i18n.translate('xpack.idxMgmt.templateBadgeType.system', { defaultMessage: 'System' }),
+};
+
+export const TemplateTypeIndicator = ({ templateType }: Props) => {
+ if (templateType === 'default') {
+ return null;
+ }
+
+ return (
+
+ {i18nTexts[templateType]}
+
+ );
+};
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx
index b470bcfd7660e..9203e76fce787 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx
@@ -7,7 +7,7 @@
import React, { useState, Fragment } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiInMemoryTable, EuiIcon, EuiButton, EuiLink, EuiBasicTableColumn } from '@elastic/eui';
+import { EuiInMemoryTable, EuiButton, EuiLink, EuiBasicTableColumn } from '@elastic/eui';
import { ScopedHistory } from 'kibana/public';
import { SendRequestResponse, reactRouterNavigate } from '../../../../../../shared_imports';
import { TemplateListItem } from '../../../../../../../common';
@@ -15,6 +15,8 @@ import { UIM_TEMPLATE_SHOW_DETAILS_CLICK } from '../../../../../../../common/con
import { TemplateDeleteModal } from '../../../../../components';
import { encodePathForReactRouter } from '../../../../../services/routing';
import { useServices } from '../../../../../app_context';
+import { TemplateContentIndicator } from '../../../../../components/shared';
+import { TemplateTypeIndicator } from '../../components';
interface Props {
templates: TemplateListItem[];
@@ -47,20 +49,23 @@ export const LegacyTemplateTable: React.FunctionComponent = ({
sortable: true,
render: (name: TemplateListItem['name'], item: TemplateListItem) => {
return (
- /* eslint-disable-next-line @elastic/eui/href-or-on-click */
- uiMetricService.trackMetric('click', UIM_TEMPLATE_SHOW_DETAILS_CLICK)
- )}
- data-test-subj="templateDetailsLink"
- >
- {name}
-
+ <>
+ uiMetricService.trackMetric('click', UIM_TEMPLATE_SHOW_DETAILS_CLICK)
+ )}
+ data-test-subj="templateDetailsLink"
+ >
+ {name}
+
+
+
+ >
);
},
},
@@ -98,44 +103,30 @@ export const LegacyTemplateTable: React.FunctionComponent = ({
) : null,
},
{
- field: 'order',
- name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.orderColumnTitle', {
- defaultMessage: 'Order',
- }),
- truncateText: true,
- sortable: true,
- },
- {
- field: 'hasMappings',
- name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.mappingsColumnTitle', {
- defaultMessage: 'Mappings',
+ name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.contentColumnTitle', {
+ defaultMessage: 'Content',
}),
- truncateText: true,
- sortable: true,
- render: (hasMappings: boolean) => (hasMappings ? : null),
- },
- {
- field: 'hasSettings',
- name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.settingsColumnTitle', {
- defaultMessage: 'Settings',
- }),
- truncateText: true,
- sortable: true,
- render: (hasSettings: boolean) => (hasSettings ? : null),
- },
- {
- field: 'hasAliases',
- name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.aliasesColumnTitle', {
- defaultMessage: 'Aliases',
- }),
- truncateText: true,
- sortable: true,
- render: (hasAliases: boolean) => (hasAliases ? : null),
+ width: '120px',
+ render: (item: TemplateListItem) => (
+
+ {i18n.translate('xpack.idxMgmt.templateList.table.noneDescriptionText', {
+ defaultMessage: 'None',
+ })}
+
+ }
+ />
+ ),
},
{
name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.actionColumnTitle', {
defaultMessage: 'Actions',
}),
+ width: '120px',
actions: [
{
name: i18n.translate('xpack.idxMgmt.templateList.legacyTable.actionEditText', {
@@ -153,7 +144,7 @@ export const LegacyTemplateTable: React.FunctionComponent = ({
onClick: ({ name }: TemplateListItem) => {
editTemplate(name, true);
},
- enabled: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged,
+ enabled: ({ _kbnMeta: { type } }: TemplateListItem) => type !== 'cloudManaged',
},
{
type: 'icon',
@@ -188,7 +179,7 @@ export const LegacyTemplateTable: React.FunctionComponent = ({
setTemplatesToDelete([{ name, isLegacy }]);
},
isPrimary: true,
- enabled: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged,
+ enabled: ({ _kbnMeta: { type } }: TemplateListItem) => type !== 'cloudManaged',
},
],
},
@@ -208,13 +199,13 @@ export const LegacyTemplateTable: React.FunctionComponent = ({
const selectionConfig = {
onSelectionChange: setSelection,
- selectable: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged,
+ selectable: ({ _kbnMeta: { type } }: TemplateListItem) => type !== 'cloudManaged',
selectableMessage: (selectable: boolean) => {
if (!selectable) {
return i18n.translate(
- 'xpack.idxMgmt.templateList.legacyTable.deleteManagedTemplateTooltip',
+ 'xpack.idxMgmt.templateList.legacyTable.deleteCloudManagedTemplateTooltip',
{
- defaultMessage: 'You cannot delete a managed template.',
+ defaultMessage: 'You cannot delete a cloud-managed template.',
}
);
}
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx
index fe6c9ad3d8e07..0c403e69d2e76 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/tabs/tab_summary.tsx
@@ -17,9 +17,11 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiCodeBlock,
+ EuiSpacer,
} from '@elastic/eui';
+import { useAppContext } from '../../../../../app_context';
import { TemplateDeserialized } from '../../../../../../../common';
-import { getILMPolicyPath } from '../../../../../services/navigation';
+import { getILMPolicyPath } from '../../../../../services/routing';
interface Props {
templateDetails: TemplateDeserialized;
@@ -51,158 +53,174 @@ export const TabSummary: React.FunctionComponent = ({ templateDetails })
const numIndexPatterns = indexPatterns.length;
+ const {
+ core: { getUrlForApp },
+ } = useAppContext();
+
return (
-
-
-
- {/* Index patterns */}
-
-
-
-
- {numIndexPatterns > 1 ? (
-
-
- {indexPatterns.map((indexName: string, i: number) => {
- return (
-
-
- {indexName}
-
-
- );
- })}
-
-
+ <>
+
+
+
+ {/* Index patterns */}
+
+
+
+
+ {numIndexPatterns > 1 ? (
+
+
+ {indexPatterns.map((indexName: string, i: number) => {
+ return (
+
+
+ {indexName}
+
+
+ );
+ })}
+
+
+ ) : (
+ indexPatterns.toString()
+ )}
+
+
+ {/* Priority / Order */}
+ {isLegacy !== true ? (
+ <>
+
+
+
+
+ {priority || priority === 0 ? priority : i18nTexts.none}
+
+ >
) : (
- indexPatterns.toString()
+ <>
+
+
+
+
+ {order || order === 0 ? order : i18nTexts.none}
+
+ >
)}
-
-
- {/* Priority / Order */}
- {isLegacy !== true ? (
- <>
-
-
-
-
- {priority || priority === 0 ? priority : i18nTexts.none}
-
- >
- ) : (
- <>
-
-
-
-
- {order || order === 0 ? order : i18nTexts.none}
-
- >
- )}
- {/* Components */}
- {isLegacy !== true && (
- <>
-
-
-
-
- {composedOf && composedOf.length > 0 ? (
-
- {composedOf.map((component) => (
-
-
- {component}
-
-
- ))}
-
- ) : (
- i18nTexts.none
- )}
-
- >
- )}
-
-
+ {/* Components */}
+ {isLegacy !== true && (
+ <>
+
+
+
+
+ {composedOf && composedOf.length > 0 ? (
+
+ {composedOf.map((component) => (
+
+
+ {component}
+
+
+ ))}
+
+ ) : (
+ i18nTexts.none
+ )}
+
+ >
+ )}
+
+
-
-
- {/* ILM Policy (only for legacy as composable template could have ILM policy
+
+
+ {/* ILM Policy (only for legacy as composable template could have ILM policy
inside one of their components) */}
- {isLegacy && (
- <>
-
-
-
-
- {ilmPolicy && ilmPolicy.name ? (
- {ilmPolicy.name}
- ) : (
- i18nTexts.none
- )}
-
- >
- )}
+ {isLegacy && (
+ <>
+
+
+
+
+ {ilmPolicy && ilmPolicy.name ? (
+
+ {ilmPolicy.name}
+
+ ) : (
+ i18nTexts.none
+ )}
+
+ >
+ )}
+
+ {/* Has data stream? (only for composable template) */}
+ {isLegacy !== true && (
+ <>
+
+
+
+
+ {hasDatastream ? i18nTexts.yes : i18nTexts.no}
+
+ >
+ )}
- {/* Has data stream? (only for composable template) */}
- {isLegacy !== true && (
- <>
-
-
-
-
- {hasDatastream ? i18nTexts.yes : i18nTexts.no}
-
- >
- )}
+ {/* Version */}
+
+
+
+
+ {version || version === 0 ? version : i18nTexts.none}
+
+
+
+
- {/* Version */}
-
-
-
-
- {version || version === 0 ? version : i18nTexts.none}
-
+
- {/* Metadata (optional) */}
- {isLegacy !== true && _meta && (
- <>
-
-
-
-
- {JSON.stringify(_meta, null, 2)}
-
- >
- )}
-
-
-
+
+ {/* Metadata (optional) */}
+ {isLegacy !== true && _meta && (
+ <>
+
+
+
+
+ {JSON.stringify(_meta, null, 2)}
+
+ >
+ )}
+
+ >
);
};
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx
index 34e90aef51701..5b726013a1d92 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx
@@ -36,6 +36,7 @@ import { useLoadIndexTemplate } from '../../../../services/api';
import { decodePathFromReactRouter } from '../../../../services/routing';
import { useServices } from '../../../../app_context';
import { TabAliases, TabMappings, TabSettings } from '../../../../components/shared';
+import { TemplateTypeIndicator } from '../components';
import { TabSummary } from './tabs';
const SUMMARY_TAB_ID = 'summary';
@@ -98,7 +99,7 @@ export const TemplateDetailsContent = ({
decodedTemplateName,
isLegacy
);
- const isCloudManaged = templateDetails?._kbnMeta.isCloudManaged ?? false;
+ const isCloudManaged = templateDetails?._kbnMeta.type === 'cloudManaged';
const [templateToDelete, setTemplateToDelete] = useState<
Array<{ name: string; isLegacy?: boolean }>
>([]);
@@ -111,6 +112,12 @@ export const TemplateDetailsContent = ({
{decodedTemplateName}
+ {templateDetails && (
+ <>
+
+
+ >
+ )}
@@ -163,16 +170,16 @@ export const TemplateDetailsContent = ({
}
color="primary"
size="s"
>
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx
index 18a65407ee20d..f421bc5d87a54 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx
@@ -37,13 +37,19 @@ import { TemplateDetails } from './template_details';
import { LegacyTemplateTable } from './legacy_templates/template_table';
import { FilterListButton, Filters } from './components';
-type FilterName = 'composable' | 'system';
+type FilterName = 'managed' | 'cloudManaged' | 'system';
interface MatchParams {
templateName?: string;
}
-const stripOutSystemTemplates = (templates: TemplateListItem[]): TemplateListItem[] =>
- templates.filter((template) => !template.name.startsWith('.'));
+function filterTemplates(templates: TemplateListItem[], types: string[]): TemplateListItem[] {
+ return templates.filter((template) => {
+ if (template._kbnMeta.type === 'default') {
+ return true;
+ }
+ return types.includes(template._kbnMeta.type);
+ });
+}
export const TemplateList: React.FunctionComponent> = ({
match: {
@@ -56,12 +62,18 @@ export const TemplateList: React.FunctionComponent>({
- composable: {
- name: i18n.translate('xpack.idxMgmt.indexTemplatesList.viewComposableTemplateLabel', {
- defaultMessage: 'Composable templates',
+ managed: {
+ name: i18n.translate('xpack.idxMgmt.indexTemplatesList.viewManagedTemplateLabel', {
+ defaultMessage: 'Managed templates',
}),
checked: 'on',
},
+ cloudManaged: {
+ name: i18n.translate('xpack.idxMgmt.indexTemplatesList.viewCloudManagedTemplateLabel', {
+ defaultMessage: 'Cloud-managed templates',
+ }),
+ checked: 'off',
+ },
system: {
name: i18n.translate('xpack.idxMgmt.indexTemplatesList.viewSystemTemplateLabel', {
defaultMessage: 'System templates',
@@ -72,18 +84,19 @@ export const TemplateList: React.FunctionComponent {
if (!allTemplates) {
+ // If templates are not fetched, return empty arrays.
return { templates: [], legacyTemplates: [] };
}
- return filters.system.checked === 'on'
- ? allTemplates
- : {
- templates: stripOutSystemTemplates(allTemplates.templates),
- legacyTemplates: stripOutSystemTemplates(allTemplates.legacyTemplates),
- };
- }, [allTemplates, filters.system.checked]);
+ const visibleTemplateTypes = Object.entries(filters)
+ .filter(([name, _filter]) => _filter.checked === 'on')
+ .map(([name]) => name);
- const showComposableTemplateTable = filters.composable.checked === 'on';
+ return {
+ templates: filterTemplates(allTemplates.templates, visibleTemplateTypes),
+ legacyTemplates: filterTemplates(allTemplates.legacyTemplates, visibleTemplateTypes),
+ };
+ }, [allTemplates, filters]);
const selectedTemplate = Boolean(templateName)
? {
@@ -154,8 +167,8 @@ export const TemplateList: React.FunctionComponent
);
- const renderTemplatesTable = () =>
- showComposableTemplateTable ? (
+ const renderTemplatesTable = () => {
+ return (
<>
>
- ) : null;
+ );
+ };
const renderLegacyTemplatesTable = () => (
<>
diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx
index 55a777363d06f..3dffdcde160f1 100644
--- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx
@@ -7,14 +7,7 @@
import React, { useState, Fragment } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import {
- EuiInMemoryTable,
- EuiBasicTableColumn,
- EuiButton,
- EuiLink,
- EuiBadge,
- EuiIcon,
-} from '@elastic/eui';
+import { EuiInMemoryTable, EuiBasicTableColumn, EuiButton, EuiLink, EuiIcon } from '@elastic/eui';
import { ScopedHistory } from 'kibana/public';
import { TemplateListItem } from '../../../../../../common';
@@ -24,6 +17,7 @@ import { encodePathForReactRouter } from '../../../../services/routing';
import { useServices } from '../../../../app_context';
import { TemplateDeleteModal } from '../../../../components';
import { TemplateContentIndicator } from '../../../../components/shared';
+import { TemplateTypeIndicator } from '../components';
interface Props {
templates: TemplateListItem[];
@@ -70,13 +64,7 @@ export const TemplateTable: React.FunctionComponent = ({
{name}
- {item._kbnMeta.isManaged ? (
-
- Managed
-
- ) : (
- ''
- )}
+
>
);
},
@@ -99,14 +87,6 @@ export const TemplateTable: React.FunctionComponent = ({
sortable: true,
render: (composedOf: string[] = []) => {composedOf.join(', ')} ,
},
- {
- field: 'priority',
- name: i18n.translate('xpack.idxMgmt.templateList.table.priorityColumnTitle', {
- defaultMessage: 'Priority',
- }),
- truncateText: true,
- sortable: true,
- },
{
name: i18n.translate('xpack.idxMgmt.templateList.table.dataStreamColumnTitle', {
defaultMessage: 'Data stream',
@@ -119,7 +99,7 @@ export const TemplateTable: React.FunctionComponent = ({
name: i18n.translate('xpack.idxMgmt.templateList.table.contentColumnTitle', {
defaultMessage: 'Content',
}),
- truncateText: true,
+ width: '120px',
render: (item: TemplateListItem) => (
= ({
name: i18n.translate('xpack.idxMgmt.templateList.table.actionColumnTitle', {
defaultMessage: 'Actions',
}),
+ width: '120px',
actions: [
{
name: i18n.translate('xpack.idxMgmt.templateList.table.actionEditText', {
@@ -153,7 +134,7 @@ export const TemplateTable: React.FunctionComponent = ({
onClick: ({ name }: TemplateListItem) => {
editTemplate(name);
},
- enabled: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged,
+ enabled: ({ _kbnMeta: { type } }: TemplateListItem) => type !== 'cloudManaged',
},
{
type: 'icon',
@@ -182,7 +163,7 @@ export const TemplateTable: React.FunctionComponent = ({
setTemplatesToDelete([{ name, isLegacy }]);
},
isPrimary: true,
- enabled: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged,
+ enabled: ({ _kbnMeta: { type } }: TemplateListItem) => type !== 'cloudManaged',
},
],
},
@@ -202,13 +183,13 @@ export const TemplateTable: React.FunctionComponent = ({
const selectionConfig = {
onSelectionChange: setSelection,
- selectable: ({ _kbnMeta: { isCloudManaged } }: TemplateListItem) => !isCloudManaged,
+ selectable: ({ _kbnMeta: { type } }: TemplateListItem) => type !== 'cloudManaged',
selectableMessage: (selectable: boolean) => {
if (!selectable) {
return i18n.translate(
- 'xpack.idxMgmt.templateList.legacyTable.deleteManagedTemplateTooltip',
+ 'xpack.idxMgmt.templateList.table.deleteCloudManagedTemplateTooltip',
{
- defaultMessage: 'You cannot delete a managed template.',
+ defaultMessage: 'You cannot delete a cloud-managed template.',
}
);
}
diff --git a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx
index 6ecefe18b1a61..29fd2e02120fc 100644
--- a/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx
+++ b/x-pack/plugins/index_management/public/application/sections/template_edit/template_edit.tsx
@@ -85,11 +85,11 @@ export const TemplateEdit: React.FunctionComponent {
- if (filter) {
- // React router tries to decode url params but it can't because the browser partially
- // decodes them. So we have to encode both the URL and the filter to get it all to
- // work correctly for filters with URL unsafe characters in them.
- return encodeURI(`/indices/filter/${encodeURIComponent(filter)}`);
- }
-
- // If no filter, URI is already safe so no need to encode.
- return '/indices';
-};
-
-export const getILMPolicyPath = (policyName: string) => {
- return encodeURI(`/policies/edit/${encodeURIComponent(policyName)}`);
-};
diff --git a/x-pack/plugins/index_management/public/application/services/routing.ts b/x-pack/plugins/index_management/public/application/services/routing.ts
index 8831fa2368f47..68bf06409e6ab 100644
--- a/x-pack/plugins/index_management/public/application/services/routing.ts
+++ b/x-pack/plugins/index_management/public/application/services/routing.ts
@@ -31,6 +31,28 @@ export const getTemplateCloneLink = (name: string, isLegacy?: boolean) => {
return encodeURI(url);
};
+export const getILMPolicyPath = (policyName: string) => {
+ return encodeURI(
+ `/data/index_lifecycle_management/policies/edit/${encodeURIComponent(policyName)}`
+ );
+};
+
+export const getIndexListUri = (filter?: string, includeHiddenIndices?: boolean) => {
+ const hiddenIndicesParam =
+ typeof includeHiddenIndices !== 'undefined' ? includeHiddenIndices : false;
+ if (filter) {
+ // React router tries to decode url params but it can't because the browser partially
+ // decodes them. So we have to encode both the URL and the filter to get it all to
+ // work correctly for filters with URL unsafe characters in them.
+ return encodeURI(
+ `/indices?includeHiddenIndices=${hiddenIndicesParam}&filter=${encodeURIComponent(filter)}`
+ );
+ }
+
+ // If no filter, URI is already safe so no need to encode.
+ return '/indices';
+};
+
export const decodePathFromReactRouter = (pathname: string): string => {
let decodedPath;
try {
diff --git a/x-pack/plugins/index_management/public/index.ts b/x-pack/plugins/index_management/public/index.ts
index 7a76fff7f3ec6..a2e9a41feb165 100644
--- a/x-pack/plugins/index_management/public/index.ts
+++ b/x-pack/plugins/index_management/public/index.ts
@@ -13,4 +13,4 @@ export const plugin = () => {
export { IndexManagementPluginSetup };
-export { getIndexListUri } from './application/services/navigation';
+export { getIndexListUri } from './application/services/routing';
diff --git a/x-pack/plugins/index_management/public/shared_imports.ts b/x-pack/plugins/index_management/public/shared_imports.ts
index 5bf1a31d0902b..3f7fcf424f1f0 100644
--- a/x-pack/plugins/index_management/public/shared_imports.ts
+++ b/x-pack/plugins/index_management/public/shared_imports.ts
@@ -22,6 +22,8 @@ export {
useForm,
Form,
getUseField,
+ UseField,
+ FormDataProvider,
} from '../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib';
export {
@@ -33,6 +35,7 @@ export {
export {
getFormRow,
Field,
+ ToggleField,
JsonEditorField,
} from '../../../../src/plugins/es_ui_shared/static/forms/components';
diff --git a/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts b/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts
index 5f4e625348333..b91c7b4650180 100644
--- a/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts
+++ b/x-pack/plugins/index_management/server/routes/api/data_streams/register_get_route.ts
@@ -17,7 +17,9 @@ export function registerGetAllRoute({ router, license, lib: { isEsError } }: Rou
const { callAsCurrentUser } = ctx.dataManagement!.client;
try {
- const dataStreams = await callAsCurrentUser('dataManagement.getDataStreams');
+ const { data_streams: dataStreams } = await callAsCurrentUser(
+ 'dataManagement.getDataStreams'
+ );
const body = deserializeDataStreamList(dataStreams);
return res.ok({ body });
@@ -50,7 +52,10 @@ export function registerGetOneRoute({ router, license, lib: { isEsError } }: Rou
const { callAsCurrentUser } = ctx.dataManagement!.client;
try {
- const dataStream = await callAsCurrentUser('dataManagement.getDataStream', { name });
+ const { data_streams: dataStream } = await callAsCurrentUser(
+ 'dataManagement.getDataStream',
+ { name }
+ );
if (dataStream[0]) {
const body = deserializeDataStream(dataStream[0]);
diff --git a/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts b/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts
index c905f92d70541..18c74716a35b6 100644
--- a/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts
+++ b/x-pack/plugins/index_management/server/routes/api/templates/validate_schemas.ts
@@ -20,6 +20,7 @@ export const templateSchema = schema.object({
})
),
composedOf: schema.maybe(schema.arrayOf(schema.string())),
+ dataStream: schema.maybe(schema.object({}, { unknowns: 'allow' })),
_meta: schema.maybe(schema.object({}, { unknowns: 'allow' })),
ilmPolicy: schema.maybe(
schema.object({
@@ -28,8 +29,7 @@ export const templateSchema = schema.object({
})
),
_kbnMeta: schema.object({
- isManaged: schema.maybe(schema.boolean()),
- isCloudManaged: schema.maybe(schema.boolean()),
+ type: schema.string(),
hasDatastream: schema.maybe(schema.boolean()),
isLegacy: schema.maybe(schema.boolean()),
}),
diff --git a/x-pack/plugins/index_management/test/fixtures/template.ts b/x-pack/plugins/index_management/test/fixtures/template.ts
index 1a44ac0f71f20..3b9de2b3409b6 100644
--- a/x-pack/plugins/index_management/test/fixtures/template.ts
+++ b/x-pack/plugins/index_management/test/fixtures/template.ts
@@ -5,7 +5,11 @@
*/
import { getRandomString, getRandomNumber } from '../../../../test_utils';
-import { TemplateDeserialized } from '../../common';
+import { TemplateDeserialized, TemplateType, TemplateListItem } from '../../common';
+
+const objHasProperties = (obj?: Record): boolean => {
+ return obj === undefined || Object.keys(obj).length === 0 ? false : true;
+};
export const getTemplate = ({
name = getRandomString(),
@@ -13,31 +17,35 @@ export const getTemplate = ({
order = getRandomNumber(),
indexPatterns = [],
template: { settings, aliases, mappings } = {},
- isManaged = false,
- isCloudManaged = false,
hasDatastream = false,
isLegacy = false,
+ type = 'default',
}: Partial<
TemplateDeserialized & {
isLegacy?: boolean;
- isManaged: boolean;
- isCloudManaged: boolean;
+ type?: TemplateType;
hasDatastream: boolean;
}
-> = {}): TemplateDeserialized => ({
- name,
- version,
- order,
- indexPatterns,
- template: {
- aliases,
- mappings,
- settings,
- },
- _kbnMeta: {
- isManaged,
- isCloudManaged,
- hasDatastream,
- isLegacy,
- },
-});
+> = {}): TemplateDeserialized & TemplateListItem => {
+ const indexTemplate = {
+ name,
+ version,
+ order,
+ indexPatterns,
+ template: {
+ aliases,
+ mappings,
+ settings,
+ },
+ hasSettings: objHasProperties(settings),
+ hasMappings: objHasProperties(mappings),
+ hasAliases: objHasProperties(aliases),
+ _kbnMeta: {
+ type,
+ hasDatastream,
+ isLegacy,
+ },
+ };
+
+ return indexTemplate;
+};
diff --git a/x-pack/plugins/infra/kibana.json b/x-pack/plugins/infra/kibana.json
index e5ce1b1cd96f8..06394c2aa916c 100644
--- a/x-pack/plugins/infra/kibana.json
+++ b/x-pack/plugins/infra/kibana.json
@@ -16,5 +16,12 @@
"optionalPlugins": ["ml", "observability"],
"server": true,
"ui": true,
- "configPath": ["xpack", "infra"]
+ "configPath": ["xpack", "infra"],
+ "requiredBundles": [
+ "observability",
+ "licenseManagement",
+ "kibanaUtils",
+ "kibanaReact",
+ "apm"
+ ]
}
diff --git a/x-pack/plugins/infra/public/__snapshots__/metrics_overview_fetchers.test.ts.snap b/x-pack/plugins/infra/public/__snapshots__/metrics_overview_fetchers.test.ts.snap
index 99ab129fc36e3..4680414493a2c 100644
--- a/x-pack/plugins/infra/public/__snapshots__/metrics_overview_fetchers.test.ts.snap
+++ b/x-pack/plugins/infra/public/__snapshots__/metrics_overview_fetchers.test.ts.snap
@@ -91,7 +91,6 @@ Object {
"y": 3.5,
},
],
- "label": "Inbound traffic",
},
"outboundTraffic": Object {
"coordinates": Array [
@@ -180,32 +179,26 @@ Object {
"y": 4,
},
],
- "label": "Outbound traffic",
},
},
"stats": Object {
"cpu": Object {
- "label": "CPU usage",
"type": "percent",
"value": 0.0015,
},
"hosts": Object {
- "label": "Hosts",
"type": "number",
"value": 2,
},
"inboundTraffic": Object {
- "label": "Inbound traffic",
"type": "bytesPerSecond",
"value": 3.5,
},
"memory": Object {
- "label": "Memory usage",
"type": "percent",
"value": 0.0015,
},
"outboundTraffic": Object {
- "label": "Outbound traffic",
"type": "bytesPerSecond",
"value": 3,
},
diff --git a/x-pack/plugins/infra/public/metrics_overview_fetchers.ts b/x-pack/plugins/infra/public/metrics_overview_fetchers.ts
index 15751fab39abc..25b334d03c4f7 100644
--- a/x-pack/plugins/infra/public/metrics_overview_fetchers.ts
+++ b/x-pack/plugins/infra/public/metrics_overview_fetchers.ts
@@ -103,14 +103,6 @@ export const createMetricsFetchData = (
body: JSON.stringify(snapshotRequest),
});
- const inboundLabel = i18n.translate('xpack.infra.observabilityHomepage.metrics.rxLabel', {
- defaultMessage: 'Inbound traffic',
- });
-
- const outboundLabel = i18n.translate('xpack.infra.observabilityHomepage.metrics.txLabel', {
- defaultMessage: 'Outbound traffic',
- });
-
return {
title: i18n.translate('xpack.infra.observabilityHomepage.metrics.title', {
defaultMessage: 'Metrics',
@@ -119,43 +111,30 @@ export const createMetricsFetchData = (
stats: {
hosts: {
type: 'number',
- label: i18n.translate('xpack.infra.observabilityHomepage.metrics.hostsLabel', {
- defaultMessage: 'Hosts',
- }),
value: results.nodes.length,
},
cpu: {
type: 'percent',
- label: i18n.translate('xpack.infra.observabilityHomepage.metrics.cpuLabel', {
- defaultMessage: 'CPU usage',
- }),
value: combineNodesBy('cpu', results.nodes, average),
},
memory: {
type: 'percent',
- label: i18n.translate('xpack.infra.observabilityHomepage.metrics.memoryLabel', {
- defaultMessage: 'Memory usage',
- }),
value: combineNodesBy('memory', results.nodes, average),
},
inboundTraffic: {
type: 'bytesPerSecond',
- label: inboundLabel,
value: combineNodesBy('rx', results.nodes, average),
},
outboundTraffic: {
type: 'bytesPerSecond',
- label: outboundLabel,
value: combineNodesBy('tx', results.nodes, average),
},
},
series: {
inboundTraffic: {
- label: inboundLabel,
coordinates: combineNodeTimeseriesBy('rx', results.nodes, average),
},
outboundTraffic: {
- label: outboundLabel,
coordinates: combineNodeTimeseriesBy('tx', results.nodes, average),
},
},
diff --git a/x-pack/plugins/infra/public/utils/datemath.test.ts b/x-pack/plugins/infra/public/utils/datemath.test.ts
index c8fbe5583db2e..e073afb231b0b 100644
--- a/x-pack/plugins/infra/public/utils/datemath.test.ts
+++ b/x-pack/plugins/infra/public/utils/datemath.test.ts
@@ -196,6 +196,15 @@ describe('extendDatemath()', () => {
diffUnit: 'y',
});
});
+
+ it('Returns no difference if the next value would result in an epoch smaller than 0', () => {
+ // FIXME: Test will fail in ~551 years
+ expect(extendDatemath('now-500y', 'before')).toBeUndefined();
+
+ expect(
+ extendDatemath('1970-01-01T00:00:00.000Z', 'before', '1970-01-01T00:00:00.001Z')
+ ).toBeUndefined();
+ });
});
describe('with a positive operator', () => {
@@ -573,6 +582,13 @@ describe('extendDatemath()', () => {
diffUnit: 'y',
});
});
+
+ it('Returns no difference if the next value would result in an epoch bigger than the max JS date', () => {
+ expect(extendDatemath('now+275760y', 'after')).toBeUndefined();
+ expect(
+ extendDatemath('+275760-09-13T00:00:00.000Z', 'after', '+275760-09-12T23:59:59.999Z')
+ ).toBeUndefined();
+ });
});
});
});
diff --git a/x-pack/plugins/infra/public/utils/datemath.ts b/x-pack/plugins/infra/public/utils/datemath.ts
index f2bd5d94ac2c3..791fe4bdb8da7 100644
--- a/x-pack/plugins/infra/public/utils/datemath.ts
+++ b/x-pack/plugins/infra/public/utils/datemath.ts
@@ -6,6 +6,8 @@
import dateMath, { Unit } from '@elastic/datemath';
+const JS_MAX_DATE = 8640000000000000;
+
export function isValidDatemath(value: string): boolean {
const parsedValue = dateMath.parse(value);
return !!(parsedValue && parsedValue.isValid());
@@ -136,18 +138,24 @@ function extendRelativeDatemath(
// if `diffAmount` is not an integer after normalization, express the difference in the original unit
const shouldKeepDiffUnit = diffAmount % 1 !== 0;
- return {
- value: `now${operator}${normalizedAmount}${normalizedUnit}`,
- diffUnit: shouldKeepDiffUnit ? unit : newUnit,
- diffAmount: shouldKeepDiffUnit ? Math.abs(newAmount - parsedAmount) : diffAmount,
- };
+ const nextValue = `now${operator}${normalizedAmount}${normalizedUnit}`;
+
+ if (isDateInRange(nextValue)) {
+ return {
+ value: nextValue,
+ diffUnit: shouldKeepDiffUnit ? unit : newUnit,
+ diffAmount: shouldKeepDiffUnit ? Math.abs(newAmount - parsedAmount) : diffAmount,
+ };
+ } else {
+ return undefined;
+ }
}
function extendAbsoluteDatemath(
value: string,
direction: 'before' | 'after',
oppositeEdge: string
-): DatemathExtension {
+): DatemathExtension | undefined {
const valueTimestamp = datemathToEpochMillis(value)!;
const oppositeEdgeTimestamp = datemathToEpochMillis(oppositeEdge)!;
const actualTimestampDiff = Math.abs(valueTimestamp - oppositeEdgeTimestamp);
@@ -159,11 +167,15 @@ function extendAbsoluteDatemath(
? valueTimestamp - normalizedTimestampDiff
: valueTimestamp + normalizedTimestampDiff;
- return {
- value: new Date(newValue).toISOString(),
- diffUnit: normalizedDiff.unit,
- diffAmount: normalizedDiff.amount,
- };
+ if (isDateInRange(newValue)) {
+ return {
+ value: new Date(newValue).toISOString(),
+ diffUnit: normalizedDiff.unit,
+ diffAmount: normalizedDiff.amount,
+ };
+ } else {
+ return undefined;
+ }
}
const CONVERSION_RATIOS: Record> = {
@@ -265,3 +277,12 @@ export function normalizeDate(amount: number, unit: Unit): { amount: number; uni
// Cannot go one one unit above. Return as it is
return { amount, unit };
}
+
+function isDateInRange(date: string | number): boolean {
+ try {
+ const epoch = typeof date === 'string' ? datemathToEpochMillis(date) ?? -1 : date;
+ return epoch >= 0 && epoch <= JS_MAX_DATE;
+ } catch {
+ return false;
+ }
+}
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts
index de5eda4a1f2c3..7f6bf9551e2c1 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts
@@ -23,6 +23,7 @@ interface Aggregation {
buckets: Array<{
aggregatedValue: { value: number; values?: Array<{ key: number; value: number }> };
doc_count: number;
+ key_as_string: string;
}>;
};
}
@@ -57,17 +58,18 @@ export const evaluateAlert = (
);
const { threshold, comparator } = criterion;
const comparisonFunction = comparatorMap[comparator];
- return mapValues(currentValues, (values: number | number[] | null) => {
- if (isTooManyBucketsPreviewException(values)) throw values;
+ return mapValues(currentValues, (points: any[] | typeof NaN | null) => {
+ if (isTooManyBucketsPreviewException(points)) throw points;
return {
...criterion,
metric: criterion.metric ?? DOCUMENT_COUNT_I18N,
- currentValue: Array.isArray(values) ? last(values) : NaN,
- shouldFire: Array.isArray(values)
- ? values.map((value) => comparisonFunction(value, threshold))
+ currentValue: Array.isArray(points) ? last(points)?.value : NaN,
+ timestamp: Array.isArray(points) ? last(points)?.key : NaN,
+ shouldFire: Array.isArray(points)
+ ? points.map((point) => comparisonFunction(point.value, threshold))
: [false],
- isNoData: values === null,
- isError: isNaN(values),
+ isNoData: points === null,
+ isError: isNaN(points),
};
});
})
@@ -157,17 +159,20 @@ const getValuesFromAggregations = (
const { buckets } = aggregations.aggregatedIntervals;
if (!buckets.length) return null; // No Data state
if (aggType === Aggregators.COUNT) {
- return buckets.map((bucket) => bucket.doc_count);
+ return buckets.map((bucket) => ({ key: bucket.key_as_string, value: bucket.doc_count }));
}
if (aggType === Aggregators.P95 || aggType === Aggregators.P99) {
return buckets.map((bucket) => {
const values = bucket.aggregatedValue?.values || [];
const firstValue = first(values);
if (!firstValue) return null;
- return firstValue.value;
+ return { key: bucket.key_as_string, value: firstValue.value };
});
}
- return buckets.map((bucket) => bucket.aggregatedValue.value);
+ return buckets.map((bucket) => ({
+ key: bucket.key_as_string,
+ value: bucket.aggregatedValue.value,
+ }));
} catch (e) {
return NaN; // Error state
}
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts
index 3ad1031f574e2..b4fe8f053a44a 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.test.ts
@@ -56,4 +56,26 @@ describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => {
);
});
});
+
+ describe('handles time', () => {
+ const end = new Date('2020-07-08T22:07:27.235Z').valueOf();
+ const timerange = {
+ end,
+ start: end - 5 * 60 * 1000,
+ };
+ const searchBody = getElasticsearchMetricQuery(
+ expressionParams,
+ timefield,
+ undefined,
+ undefined,
+ timerange
+ );
+ test('by rounding timestamps to the nearest timeUnit', () => {
+ const rangeFilter = searchBody.query.bool.filter.find((filter) =>
+ filter.hasOwnProperty('range')
+ )?.range[timefield];
+ expect(rangeFilter?.lte).toBe(1594246020000);
+ expect(rangeFilter?.gte).toBe(1594245720000);
+ });
+ });
});
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts
index 15506a30529c4..078ca46d42e60 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts
@@ -3,9 +3,11 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
+
import { networkTraffic } from '../../../../../common/inventory_models/shared/metrics/snapshot/network_traffic';
import { MetricExpressionParams, Aggregators } from '../types';
import { getIntervalInSeconds } from '../../../../utils/get_interval_in_seconds';
+import { roundTimestamp } from '../../../../utils/round_timestamp';
import { getDateHistogramOffset } from '../../../snapshot/query_helpers';
import { createPercentileAggregation } from './create_percentile_aggregation';
@@ -34,12 +36,15 @@ export const getElasticsearchMetricQuery = (
const interval = `${timeSize}${timeUnit}`;
const intervalAsSeconds = getIntervalInSeconds(interval);
- const to = timeframe ? timeframe.end : Date.now();
+ const to = roundTimestamp(timeframe ? timeframe.end : Date.now(), timeUnit);
// We need enough data for 5 buckets worth of data. We also need
// to convert the intervalAsSeconds to milliseconds.
const minimumFrom = to - intervalAsSeconds * 1000 * MINIMUM_BUCKETS;
- const from = timeframe && timeframe.start <= minimumFrom ? timeframe.start : minimumFrom;
+ const from = roundTimestamp(
+ timeframe && timeframe.start <= minimumFrom ? timeframe.start : minimumFrom,
+ timeUnit
+ );
const offset = getDateHistogramOffset(from, interval);
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts
index 24f4bc2c678b4..003a6c3c20e98 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts
@@ -94,12 +94,14 @@ describe('The metric threshold alert type', () => {
expect(getState(instanceID).alertState).toBe(AlertStates.OK);
});
test('reports expected values to the action context', async () => {
+ const now = 1577858400000;
await execute(Comparator.GT, [0.75]);
const { action } = mostRecentAction(instanceID);
expect(action.group).toBe('*');
expect(action.reason).toContain('current value is 1');
expect(action.reason).toContain('threshold of 0.75');
expect(action.reason).toContain('test.metric.1');
+ expect(action.timestamp).toBe(new Date(now).toISOString());
});
});
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
index 4c02593dd0095..bc1cc24f65eeb 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts
@@ -76,11 +76,13 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs, alertId: s
}
}
if (reason) {
+ const firstResult = first(alertResults);
+ const timestamp = (firstResult && firstResult[group].timestamp) ?? moment().toISOString();
alertInstance.scheduleActions(FIRED_ACTIONS.id, {
group,
alertState: stateToAlertMessage[nextState],
reason,
- timestamp: moment().toISOString(),
+ timestamp,
value: mapToConditionsLookup(alertResults, (result) => result[group].currentValue),
threshold: mapToConditionsLookup(criteria, (c) => c.threshold),
metric: mapToConditionsLookup(criteria, (c) => c.metric),
diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts
index ee2cf94a2fd62..c7e53eb2008f5 100644
--- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts
+++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/test_mocks.ts
@@ -12,6 +12,7 @@ const bucketsA = [
{
doc_count: 3,
aggregatedValue: { value: 1.0, values: [{ key: 95.0, value: 1.0 }] },
+ key_as_string: new Date(1577858400000).toISOString(),
},
];
diff --git a/x-pack/plugins/infra/server/utils/round_timestamp.ts b/x-pack/plugins/infra/server/utils/round_timestamp.ts
new file mode 100644
index 0000000000000..9b5ae2ac40197
--- /dev/null
+++ b/x-pack/plugins/infra/server/utils/round_timestamp.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { Unit } from '@elastic/datemath';
+import moment from 'moment';
+
+export const roundTimestamp = (timestamp: number, unit: Unit) => {
+ const floor = moment(timestamp).startOf(unit).valueOf();
+ const ceil = moment(timestamp).add(1, unit).startOf(unit).valueOf();
+ if (Math.abs(timestamp - floor) <= Math.abs(timestamp - ceil)) return floor;
+ return ceil;
+};
diff --git a/x-pack/plugins/ingest_manager/common/mocks.ts b/x-pack/plugins/ingest_manager/common/mocks.ts
new file mode 100644
index 0000000000000..131917af44595
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/common/mocks.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { NewPackageConfig, PackageConfig } from './types/models/package_config';
+
+export const createNewPackageConfigMock = () => {
+ return {
+ name: 'endpoint-1',
+ description: '',
+ namespace: 'default',
+ enabled: true,
+ config_id: '93c46720-c217-11ea-9906-b5b8a21b268e',
+ output_id: '',
+ package: {
+ name: 'endpoint',
+ title: 'Elastic Endpoint',
+ version: '0.9.0',
+ },
+ inputs: [],
+ } as NewPackageConfig;
+};
+
+export const createPackageConfigMock = () => {
+ const newPackageConfig = createNewPackageConfigMock();
+ return {
+ ...newPackageConfig,
+ id: 'c6d16e42-c32d-4dce-8a88-113cfe276ad1',
+ version: 'abcd',
+ revision: 1,
+ updated_at: '2020-06-25T16:03:38.159292',
+ updated_by: 'kibana',
+ created_at: '2020-06-25T16:03:38.159292',
+ created_by: 'kibana',
+ inputs: [
+ {
+ config: {},
+ },
+ ],
+ } as PackageConfig;
+};
diff --git a/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json b/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json
index c374cbb3bb146..4b10dab5d1ae5 100644
--- a/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json
+++ b/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json
@@ -4146,9 +4146,6 @@
"config_revision": {
"type": ["number", "null"]
},
- "config_newest_revision": {
- "type": "number"
- },
"last_checkin": {
"type": "string"
},
diff --git a/x-pack/plugins/ingest_manager/common/services/agent_status.ts b/x-pack/plugins/ingest_manager/common/services/agent_status.ts
index b1d92d3a78e65..6489c30308771 100644
--- a/x-pack/plugins/ingest_manager/common/services/agent_status.ts
+++ b/x-pack/plugins/ingest_manager/common/services/agent_status.ts
@@ -5,63 +5,52 @@
*/
import {
- AGENT_TYPE_TEMPORARY,
AGENT_POLLING_THRESHOLD_MS,
AGENT_TYPE_PERMANENT,
- AGENT_TYPE_EPHEMERAL,
AGENT_SAVED_OBJECT_TYPE,
} from '../constants';
import { Agent, AgentStatus } from '../types';
export function getAgentStatus(agent: Agent, now: number = Date.now()): AgentStatus {
- const { type, last_checkin: lastCheckIn } = agent;
- const msLastCheckIn = new Date(lastCheckIn || 0).getTime();
- const msSinceLastCheckIn = new Date().getTime() - msLastCheckIn;
- const intervalsSinceLastCheckIn = Math.floor(msSinceLastCheckIn / AGENT_POLLING_THRESHOLD_MS);
+ const { last_checkin: lastCheckIn } = agent;
+
if (!agent.active) {
return 'inactive';
}
+ if (!agent.last_checkin) {
+ return 'enrolling';
+ }
if (agent.unenrollment_started_at && !agent.unenrolled_at) {
return 'unenrolling';
}
- if (agent.current_error_events.length > 0) {
+
+ const msLastCheckIn = new Date(lastCheckIn || 0).getTime();
+ const msSinceLastCheckIn = new Date().getTime() - msLastCheckIn;
+ const intervalsSinceLastCheckIn = Math.floor(msSinceLastCheckIn / AGENT_POLLING_THRESHOLD_MS);
+
+ if (agent.last_checkin_status === 'error') {
return 'error';
}
- switch (type) {
- case AGENT_TYPE_PERMANENT:
- if (intervalsSinceLastCheckIn >= 4) {
- return 'error';
- }
- case AGENT_TYPE_TEMPORARY:
- if (intervalsSinceLastCheckIn >= 3) {
- return 'offline';
- }
- case AGENT_TYPE_EPHEMERAL:
- if (intervalsSinceLastCheckIn >= 3) {
- return 'inactive';
- }
+ if (agent.last_checkin_status === 'degraded') {
+ return 'degraded';
+ }
+ if (intervalsSinceLastCheckIn >= 4) {
+ return 'offline';
}
+
return 'online';
}
export function buildKueryForOnlineAgents() {
- return `(${AGENT_SAVED_OBJECT_TYPE}.type:${AGENT_TYPE_PERMANENT} and ${AGENT_SAVED_OBJECT_TYPE}.last_checkin >= now-${
- (4 * AGENT_POLLING_THRESHOLD_MS) / 1000
- }s) or (${AGENT_SAVED_OBJECT_TYPE}.type:${AGENT_TYPE_TEMPORARY} and ${AGENT_SAVED_OBJECT_TYPE}.last_checkin >= now-${
- (3 * AGENT_POLLING_THRESHOLD_MS) / 1000
- }s) or (${AGENT_SAVED_OBJECT_TYPE}.type:${AGENT_TYPE_EPHEMERAL} and ${AGENT_SAVED_OBJECT_TYPE}.last_checkin >= now-${
- (3 * AGENT_POLLING_THRESHOLD_MS) / 1000
- }s)`;
+ return `not (${buildKueryForOfflineAgents()}) AND not (${buildKueryForErrorAgents()})`;
}
-export function buildKueryForOfflineAgents() {
- return `${AGENT_SAVED_OBJECT_TYPE}.type:${AGENT_TYPE_TEMPORARY} AND ${AGENT_SAVED_OBJECT_TYPE}.last_checkin < now-${
- (3 * AGENT_POLLING_THRESHOLD_MS) / 1000
- }s`;
+export function buildKueryForErrorAgents() {
+ return `( ${AGENT_SAVED_OBJECT_TYPE}.last_checkin_status:error or ${AGENT_SAVED_OBJECT_TYPE}.last_checkin_status:degraded )`;
}
-export function buildKueryForErrorAgents() {
- return `${AGENT_SAVED_OBJECT_TYPE}.type:${AGENT_TYPE_PERMANENT} AND ${AGENT_SAVED_OBJECT_TYPE}.last_checkin < now-${
+export function buildKueryForOfflineAgents() {
+ return `((${AGENT_SAVED_OBJECT_TYPE}.type:${AGENT_TYPE_PERMANENT} AND ${AGENT_SAVED_OBJECT_TYPE}.last_checkin < now-${
(4 * AGENT_POLLING_THRESHOLD_MS) / 1000
- }s`;
+ }s) AND not ( ${buildKueryForErrorAgents()} ))`;
}
diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent.ts b/x-pack/plugins/ingest_manager/common/types/models/agent.ts
index 27f0c61685fd4..d3789c58a2c22 100644
--- a/x-pack/plugins/ingest_manager/common/types/models/agent.ts
+++ b/x-pack/plugins/ingest_manager/common/types/models/agent.ts
@@ -11,7 +11,16 @@ export type AgentType =
| typeof AGENT_TYPE_PERMANENT
| typeof AGENT_TYPE_TEMPORARY;
-export type AgentStatus = 'offline' | 'error' | 'online' | 'inactive' | 'warning' | 'unenrolling';
+export type AgentStatus =
+ | 'offline'
+ | 'error'
+ | 'online'
+ | 'inactive'
+ | 'warning'
+ | 'enrolling'
+ | 'unenrolling'
+ | 'degraded';
+
export type AgentActionType = 'CONFIG_CHANGE' | 'DATA_DUMP' | 'RESUME' | 'PAUSE' | 'UNENROLL';
export interface NewAgentAction {
type: AgentActionType;
@@ -81,8 +90,8 @@ interface AgentBase {
default_api_key_id?: string;
config_id?: string;
config_revision?: number | null;
- config_newest_revision?: number;
last_checkin?: string;
+ last_checkin_status?: 'error' | 'online' | 'degraded';
user_provided_metadata: AgentMetadata;
local_metadata: AgentMetadata;
}
diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts
index 23e31227cbf3c..a34038d4fba04 100644
--- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts
+++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts
@@ -42,6 +42,8 @@ export enum AgentAssetType {
input = 'input',
}
+export type RegistryRelease = 'ga' | 'beta' | 'experimental';
+
// from /package/{name}
// type Package struct at https://github.com/elastic/package-registry/blob/master/util/package.go
// https://github.com/elastic/package-registry/blob/master/docs/api/package.json
@@ -49,6 +51,7 @@ export interface RegistryPackage {
name: string;
title?: string;
version: string;
+ release?: RegistryRelease;
readme?: string;
description: string;
type: string;
@@ -114,6 +117,7 @@ export type RegistrySearchResult = Pick<
| 'name'
| 'title'
| 'version'
+ | 'release'
| 'description'
| 'type'
| 'icons'
diff --git a/x-pack/plugins/ingest_manager/common/types/models/settings.ts b/x-pack/plugins/ingest_manager/common/types/models/settings.ts
index 2921808230b47..98d99911f1b3f 100644
--- a/x-pack/plugins/ingest_manager/common/types/models/settings.ts
+++ b/x-pack/plugins/ingest_manager/common/types/models/settings.ts
@@ -10,6 +10,7 @@ interface BaseSettings {
package_auto_upgrade?: boolean;
kibana_url?: string;
kibana_ca_sha256?: string;
+ has_seen_add_data_notice?: boolean;
}
export interface Settings extends BaseSettings {
diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts
index 1105c8ee7ca82..ed7d73ab0b719 100644
--- a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts
+++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts
@@ -47,6 +47,7 @@ export interface PostAgentCheckinRequest {
agentId: string;
};
body: {
+ status?: 'online' | 'error' | 'degraded';
local_metadata?: Record;
events?: NewAgentEvent[];
};
diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts
index c5035d2d44432..1901b8c0c7039 100644
--- a/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts
+++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/epm.ts
@@ -12,13 +12,21 @@ import {
PackageInfo,
} from '../models/epm';
+export interface GetCategoriesRequest {
+ query: {
+ experimental?: boolean;
+ };
+}
+
export interface GetCategoriesResponse {
response: CategorySummaryList;
success: boolean;
}
+
export interface GetPackagesRequest {
query: {
category?: string;
+ experimental?: boolean;
};
}
diff --git a/x-pack/plugins/ingest_manager/kibana.json b/x-pack/plugins/ingest_manager/kibana.json
index 181b93a9e2425..ab0a2ba24ba66 100644
--- a/x-pack/plugins/ingest_manager/kibana.json
+++ b/x-pack/plugins/ingest_manager/kibana.json
@@ -5,6 +5,7 @@
"ui": true,
"configPath": ["xpack", "ingestManager"],
"requiredPlugins": ["licensing", "data", "encryptedSavedObjects"],
- "optionalPlugins": ["security", "features", "cloud", "usageCollection"],
- "extraPublicDirs": ["common"]
+ "optionalPlugins": ["security", "features", "cloud", "usageCollection", "home"],
+ "extraPublicDirs": ["common"],
+ "requiredBundles": ["kibanaReact", "esUiShared"]
}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/index.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/index.ts
new file mode 100644
index 0000000000000..bab6049198249
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/index.ts
@@ -0,0 +1,7 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+export { TutorialDirectoryNotice, TutorialDirectoryHeaderLink } from './tutorial_directory_notice';
+export { TutorialModuleNotice } from './tutorial_module_notice';
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_directory_notice.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_directory_notice.tsx
new file mode 100644
index 0000000000000..553623380dcc0
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_directory_notice.tsx
@@ -0,0 +1,154 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React, { memo, useState, useCallback, useEffect } from 'react';
+import { BehaviorSubject } from 'rxjs';
+import styled from 'styled-components';
+import { FormattedMessage } from '@kbn/i18n/react';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButton,
+ EuiButtonEmpty,
+ EuiLink,
+ EuiCallOut,
+ EuiSpacer,
+} from '@elastic/eui';
+import {
+ TutorialDirectoryNoticeComponent,
+ TutorialDirectoryHeaderLinkComponent,
+} from 'src/plugins/home/public';
+import { sendPutSettings, useGetSettings, useLink, useCapabilities } from '../../hooks';
+
+const FlexItemButtonWrapper = styled(EuiFlexItem)`
+ &&& {
+ margin-bottom: 0;
+ }
+`;
+
+const tutorialDirectoryNoticeState$ = new BehaviorSubject({
+ settingsDataLoaded: false,
+ hasSeenNotice: false,
+});
+
+export const TutorialDirectoryNotice: TutorialDirectoryNoticeComponent = memo(() => {
+ const { getHref } = useLink();
+ const { show: hasIngestManager } = useCapabilities();
+ const { data: settingsData, isLoading } = useGetSettings();
+ const [dismissedNotice, setDismissedNotice] = useState(false);
+
+ const dismissNotice = useCallback(async () => {
+ setDismissedNotice(true);
+ await sendPutSettings({
+ has_seen_add_data_notice: true,
+ });
+ }, []);
+
+ useEffect(() => {
+ tutorialDirectoryNoticeState$.next({
+ settingsDataLoaded: !isLoading,
+ hasSeenNotice: Boolean(dismissedNotice || settingsData?.item?.has_seen_add_data_notice),
+ });
+ }, [isLoading, settingsData, dismissedNotice]);
+
+ const hasSeenNotice =
+ isLoading || settingsData?.item?.has_seen_add_data_notice || dismissedNotice;
+
+ return hasIngestManager && !hasSeenNotice ? (
+ <>
+
+
+
+
+ ),
+ }}
+ />
+ }
+ >
+
+
+
+
+ ),
+ }}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ {
+ dismissNotice();
+ }}
+ >
+
+
+
+
+
+
+ >
+ ) : null;
+});
+
+export const TutorialDirectoryHeaderLink: TutorialDirectoryHeaderLinkComponent = memo(() => {
+ const { getHref } = useLink();
+ const { show: hasIngestManager } = useCapabilities();
+ const [noticeState, setNoticeState] = useState({
+ settingsDataLoaded: false,
+ hasSeenNotice: false,
+ });
+
+ useEffect(() => {
+ const subscription = tutorialDirectoryNoticeState$.subscribe((value) => setNoticeState(value));
+ return () => {
+ subscription.unsubscribe();
+ };
+ }, []);
+
+ return hasIngestManager && noticeState.settingsDataLoaded && noticeState.hasSeenNotice ? (
+
+
+
+ ) : null;
+});
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_module_notice.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_module_notice.tsx
new file mode 100644
index 0000000000000..a26691bdd64a0
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/components/home_integration/tutorial_module_notice.tsx
@@ -0,0 +1,74 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import React, { memo } from 'react';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiText, EuiLink, EuiSpacer } from '@elastic/eui';
+import { TutorialModuleNoticeComponent } from 'src/plugins/home/public';
+import { useGetPackages, useLink, useCapabilities } from '../../hooks';
+
+export const TutorialModuleNotice: TutorialModuleNoticeComponent = memo(({ moduleName }) => {
+ const { getHref } = useLink();
+ const { show: hasIngestManager } = useCapabilities();
+ const { data: packagesData, isLoading } = useGetPackages();
+
+ const pkgInfo =
+ !isLoading &&
+ packagesData?.response &&
+ packagesData.response.find((pkg) => pkg.name === moduleName);
+
+ if (hasIngestManager && pkgInfo) {
+ return (
+ <>
+
+
+
+
+
+
+ ),
+ availableAsIntegrationLink: (
+
+
+
+ ),
+ blogPostLink: (
+
+
+
+ ),
+ }}
+ />
+
+
+ >
+ );
+ }
+
+ return null;
+});
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_package_icon_type.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_package_icon_type.ts
index 011e0c69f2683..e5a7191372e9c 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_package_icon_type.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_package_icon_type.ts
@@ -6,7 +6,7 @@
import { useEffect, useState } from 'react';
import { ICON_TYPES } from '@elastic/eui';
-import { PackageInfo, PackageListItem } from '../../../../common/types/models';
+import { PackageInfo, PackageListItem } from '../types';
import { useLinks } from '../sections/epm/hooks';
import { sendGetPackageInfoByKey } from './index';
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/epm.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/epm.ts
index 64bee1763b08b..40a22f6b44d50 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/epm.ts
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_request/epm.ts
@@ -4,11 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { HttpFetchQuery } from 'src/core/public';
import { useRequest, sendRequest } from './use_request';
import { epmRouteService } from '../../services';
import {
+ GetCategoriesRequest,
GetCategoriesResponse,
+ GetPackagesRequest,
GetPackagesResponse,
GetLimitedPackagesResponse,
GetInfoResponse,
@@ -16,18 +17,19 @@ import {
DeletePackageResponse,
} from '../../types';
-export const useGetCategories = () => {
+export const useGetCategories = (query: GetCategoriesRequest['query'] = {}) => {
return useRequest({
path: epmRouteService.getCategoriesPath(),
method: 'get',
+ query: { experimental: true, ...query },
});
};
-export const useGetPackages = (query: HttpFetchQuery = {}) => {
+export const useGetPackages = (query: GetPackagesRequest['query'] = {}) => {
return useRequest({
path: epmRouteService.getListPath(),
method: 'get',
- query,
+ query: { experimental: true, ...query },
});
};
@@ -52,6 +54,13 @@ export const sendGetPackageInfoByKey = (pkgkey: string) => {
});
};
+export const useGetFileByPath = (filePath: string) => {
+ return useRequest({
+ path: epmRouteService.getFilePath(filePath),
+ method: 'get',
+ });
+};
+
export const sendGetFileByPath = (filePath: string) => {
return sendRequest({
path: epmRouteService.getFilePath(filePath),
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.scss b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.scss
index 5ad558dfafe7d..c732bc349687d 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.scss
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.scss
@@ -1,4 +1,4 @@
-@import '@elastic/eui/src/components/header/variables';
+@import '@elastic/eui/src/global_styling/variables/header';
@import '@elastic/eui/src/components/nav_drawer/variables';
/**
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx
index 623df428b7dd9..94d3379f35e05 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/index.tsx
@@ -22,7 +22,7 @@ import { PAGE_ROUTING_PATHS } from './constants';
import { DefaultLayout, WithoutHeaderLayout } from './layouts';
import { Loading, Error } from './components';
import { IngestManagerOverview, EPMApp, AgentConfigApp, FleetApp, DataStreamApp } from './sections';
-import { DepsContext, ConfigContext, setHttpClient, useConfig } from './hooks';
+import { DepsContext, ConfigContext, useConfig } from './hooks';
import { PackageInstallProvider } from './sections/epm/hooks';
import { useCore, sendSetup, sendGetPermissionsCheck } from './hooks';
import { FleetStatusProvider } from './hooks/use_fleet_status';
@@ -260,7 +260,6 @@ export function renderApp(
startDeps: IngestManagerStartDeps,
config: IngestManagerConfigType
) {
- setHttpClient(coreStart.http);
ReactDOM.render(
props.theme.eui.paddingSizes.m} 0;
- `;
+const FirstHeaderRow = styled(EuiFlexGroup)`
+ padding: 0 0 ${(props) => props.theme.eui.paddingSizes.m} 0;
+`;
+
+const HeaderRow = styled(EuiFlexGroup)`
+ padding: ${(props) => props.theme.eui.paddingSizes.m} 0;
+`;
- const HeaderRow = styled(EuiFlexGroup)`
- padding: ${(props) => props.theme.eui.paddingSizes.m} 0;
- `;
+const FacetGroup = styled(EuiFacetGroup)`
+ flex-grow: 0;
+`;
- const FacetGroup = styled(EuiFacetGroup)`
- flex-grow: 0;
- `;
+const FacetButton = styled(EuiFacetButton)`
+ padding: '${(props) => props.theme.eui.paddingSizes.xs} 0';
+ height: 'unset';
+`;
+export function AssetsFacetGroup({ assets }: { assets: AssetsGroupedByServiceByType }) {
return (
{entries(assets).map(([service, typeToParts], index) => {
@@ -77,10 +82,6 @@ export function AssetsFacetGroup({ assets }: { assets: AssetsGroupedByServiceByT
// only kibana assets have icons
const iconType = type in AssetIcons && AssetIcons[type];
const iconNode = iconType ? : '';
- const FacetButton = styled(EuiFacetButton)`
- padding: '${(props) => props.theme.eui.paddingSizes.xs} 0';
- height: 'unset';
- `;
return (
+ parseFloat(props.theme.eui.euiSize) * 6 + parseFloat(props.theme.eui.spacerSizes.xl) * 2}px;
+ height: 1px;
+`;
+
+const Panel = styled(EuiPanel)`
+ padding: ${(props) => props.theme.eui.spacerSizes.xl};
+ margin-bottom: -100%;
+ svg,
+ img {
+ height: ${(props) => parseFloat(props.theme.eui.euiSize) * 6}px;
+ width: ${(props) => parseFloat(props.theme.eui.euiSize) * 6}px;
+ }
+ .euiFlexItem {
+ height: ${(props) => parseFloat(props.theme.eui.euiSize) * 6}px;
+ justify-content: center;
+ }
+`;
+
+export function IconPanel({
+ packageName,
+ version,
+ icons,
+}: Pick) {
+ const iconType = usePackageIconType({ packageName, version, icons });
-export function IconPanel({ iconType }: { iconType: IconType }) {
- const Panel = styled(EuiPanel)`
- /* 🤢🤷 https://www.styled-components.com/docs/faqs#how-can-i-override-styles-with-higher-specificity */
- &&& {
- position: absolute;
- text-align: center;
- vertical-align: middle;
- padding: ${(props) => props.theme.eui.spacerSizes.xl};
- svg,
- img {
- height: ${(props) => props.theme.eui.euiKeyPadMenuSize};
- width: ${(props) => props.theme.eui.euiKeyPadMenuSize};
- }
- }
- `;
+ return (
+
+
+
+
+
+ );
+}
+export function LoadingIconPanel() {
return (
-
-
-
+
+
+
+
+
);
}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx
index acdcd5b9a3406..3f0803af6daae 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/icons.tsx
@@ -3,13 +3,20 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiIcon } from '@elastic/eui';
import React from 'react';
-import styled from 'styled-components';
+import { EuiIconTip, EuiIconProps } from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
-export const StyledAlert = styled(EuiIcon)`
- color: ${(props) => props.theme.eui.euiColorWarning};
- padding: 0 5px;
-`;
-
-export const UpdateIcon = () => ;
+export const UpdateIcon = ({ size = 'm' }: { size?: EuiIconProps['size'] }) => (
+
+);
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/nav_button_back.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/nav_button_back.tsx
deleted file mode 100644
index 3fcf9758368de..0000000000000
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/nav_button_back.tsx
+++ /dev/null
@@ -1,19 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-import { EuiButtonEmpty } from '@elastic/eui';
-import React from 'react';
-import styled from 'styled-components';
-
-export function NavButtonBack({ href, text }: { href: string; text: string }) {
- const ButtonEmpty = styled(EuiButtonEmpty)`
- margin-right: ${(props) => props.theme.eui.spacerSizes.xl};
- `;
- return (
-
- {text}
-
- );
-}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx
index e3d8cdc8f4985..cf98f9dc90230 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_card.tsx
@@ -9,12 +9,9 @@ import { EuiCard } from '@elastic/eui';
import { PackageInfo, PackageListItem } from '../../../types';
import { useLink } from '../../../hooks';
import { PackageIcon } from '../../../components/package_icon';
+import { RELEASE_BADGE_LABEL, RELEASE_BADGE_DESCRIPTION } from './release_badge';
-export interface BadgeProps {
- showInstalledBadge?: boolean;
-}
-
-type PackageCardProps = (PackageListItem | PackageInfo) & BadgeProps;
+type PackageCardProps = PackageListItem | PackageInfo;
// adding the `href` causes EuiCard to use a `a` instead of a `button`
// `a` tags use `euiLinkColor` which results in blueish Badge text
@@ -27,7 +24,7 @@ export function PackageCard({
name,
title,
version,
- showInstalledBadge,
+ release,
status,
icons,
...restProps
@@ -41,12 +38,14 @@ export function PackageCard({
return (
}
href={getHref('integration_details', { pkgkey: `${name}-${urlVersion}` })}
+ betaBadgeLabel={release && release !== 'ga' ? RELEASE_BADGE_LABEL[release] : undefined}
+ betaBadgeTooltipContent={
+ release && release !== 'ga' ? RELEASE_BADGE_DESCRIPTION[release] : undefined
+ }
/>
);
}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_list_grid.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_list_grid.tsx
index dbf454acd2b74..0c1199f7c8867 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_list_grid.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/package_list_grid.tsx
@@ -20,22 +20,16 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { Loading } from '../../../components';
import { PackageList } from '../../../types';
import { useLocalSearch, searchIdField } from '../hooks';
-import { BadgeProps, PackageCard } from './package_card';
+import { PackageCard } from './package_card';
-type ListProps = {
+interface ListProps {
isLoading?: boolean;
controls?: ReactNode;
title: string;
list: PackageList;
-} & BadgeProps;
+}
-export function PackageListGrid({
- isLoading,
- controls,
- title,
- list,
- showInstalledBadge,
-}: ListProps) {
+export function PackageListGrid({ isLoading, controls, title, list }: ListProps) {
const initialQuery = EuiSearchBar.Query.MATCH_ALL;
const [query, setQuery] = useState(initialQuery);
@@ -71,7 +65,7 @@ export function PackageListGrid({
.includes(item[searchIdField])
)
: list;
- gridContent = ;
+ gridContent = ;
}
return (
@@ -108,16 +102,16 @@ function ControlsColumn({ controls, title }: ControlsColumnProps) {
- {controls}
+ {controls}
);
}
-type GridColumnProps = {
+interface GridColumnProps {
list: PackageList;
-} & BadgeProps;
+}
function GridColumn({ list }: GridColumnProps) {
return (
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/release_badge.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/release_badge.ts
new file mode 100644
index 0000000000000..f3520b4e7a9b3
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/release_badge.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { i18n } from '@kbn/i18n';
+import { RegistryRelease } from '../../../types';
+
+export const RELEASE_BADGE_LABEL: { [key in Exclude]: string } = {
+ beta: i18n.translate('xpack.ingestManager.epm.releaseBadge.betaLabel', {
+ defaultMessage: 'Beta',
+ }),
+ experimental: i18n.translate('xpack.ingestManager.epm.releaseBadge.experimentalLabel', {
+ defaultMessage: 'Experimental',
+ }),
+};
+
+export const RELEASE_BADGE_DESCRIPTION: { [key in Exclude]: string } = {
+ beta: i18n.translate('xpack.ingestManager.epm.releaseBadge.betaDescription', {
+ defaultMessage: 'This integration is not recommended for use in production environments.',
+ }),
+ experimental: i18n.translate('xpack.ingestManager.epm.releaseBadge.experimentalDescription', {
+ defaultMessage: 'This integration may have breaking changes or be removed in a future release.',
+ }),
+};
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx
index c9a8cabdf414b..f53b4e9150ca1 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/content.tsx
@@ -16,22 +16,22 @@ import { SideNavLinks } from './side_nav_links';
import { PackageConfigsPanel } from './package_configs_panel';
import { SettingsPanel } from './settings_panel';
-type ContentProps = PackageInfo & Pick & { hasIconPanel: boolean };
-export function Content(props: ContentProps) {
- const { hasIconPanel, name, panel, version } = props;
- const SideNavColumn = hasIconPanel
- ? styled(LeftColumn)`
- /* 🤢🤷 https://www.styled-components.com/docs/faqs#how-can-i-override-styles-with-higher-specificity */
- &&& {
- margin-top: 77px;
- }
- `
- : LeftColumn;
+type ContentProps = PackageInfo & Pick;
+
+const SideNavColumn = styled(LeftColumn)`
+ /* 🤢🤷 https://www.styled-components.com/docs/faqs#how-can-i-override-styles-with-higher-specificity */
+ &&& {
+ margin-top: 77px;
+ }
+`;
+
+// fixes IE11 problem with nested flex items
+const ContentFlexGroup = styled(EuiFlexGroup)`
+ flex: 0 0 auto !important;
+`;
- // fixes IE11 problem with nested flex items
- const ContentFlexGroup = styled(EuiFlexGroup)`
- flex: 0 0 auto !important;
- `;
+export function Content(props: ContentProps) {
+ const { name, panel, version } = props;
return (
@@ -75,13 +75,13 @@ function RightColumnContent(props: RightColumnContentProps) {
const { assets, panel } = props;
switch (panel) {
case 'overview':
- return (
+ return assets ? (
- );
+ ) : null;
default:
return ;
}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx
deleted file mode 100644
index 875a8f5c5c127..0000000000000
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/header.tsx
+++ /dev/null
@@ -1,89 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-import React, { Fragment } from 'react';
-import styled from 'styled-components';
-import { i18n } from '@kbn/i18n';
-import { FormattedMessage } from '@kbn/i18n/react';
-import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiTitle, IconType, EuiButton } from '@elastic/eui';
-import { PackageInfo } from '../../../../types';
-import { useCapabilities, useLink } from '../../../../hooks';
-import { IconPanel } from '../../components/icon_panel';
-import { NavButtonBack } from '../../components/nav_button_back';
-import { CenterColumn, LeftColumn, RightColumn } from './layout';
-import { UpdateIcon } from '../../components/icons';
-
-const FullWidthNavRow = styled(EuiPage)`
- /* no left padding so link is against column left edge */
- padding-left: 0;
-`;
-
-const Text = styled.span`
- margin-right: ${(props) => props.theme.eui.euiSizeM};
-`;
-
-type HeaderProps = PackageInfo & { iconType?: IconType };
-
-export function Header(props: HeaderProps) {
- const { iconType, name, title, version, latestVersion } = props;
-
- let installedVersion;
- if ('savedObject' in props) {
- installedVersion = props.savedObject.attributes.version;
- }
- const hasWriteCapabilites = useCapabilities().write;
- const { getHref } = useLink();
- const updateAvailable = installedVersion && installedVersion < latestVersion ? true : false;
- return (
-
-
-
-
-
- {iconType ? (
-
-
-
- ) : null}
-
-
-
- {title}
-
-
- {version} {updateAvailable && }
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx
index 505687068cf42..3267fbbe3733c 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/index.tsx
@@ -3,15 +3,37 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiPage, EuiPageBody, EuiPageProps } from '@elastic/eui';
-import React, { Fragment, useEffect, useState } from 'react';
+import React, { useEffect, useState, useMemo } from 'react';
import { useParams } from 'react-router-dom';
import styled from 'styled-components';
+import { i18n } from '@kbn/i18n';
+import { FormattedMessage } from '@kbn/i18n/react';
+import {
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiButtonEmpty,
+ EuiText,
+ EuiSpacer,
+ EuiBetaBadge,
+ EuiButton,
+ EuiDescriptionList,
+ EuiDescriptionListTitle,
+ EuiDescriptionListDescription,
+} from '@elastic/eui';
import { DetailViewPanelName, InstallStatus, PackageInfo } from '../../../../types';
-import { sendGetPackageInfoByKey, usePackageIconType, useBreadcrumbs } from '../../../../hooks';
+import { Loading, Error } from '../../../../components';
+import {
+ useGetPackageInfoByKey,
+ useBreadcrumbs,
+ useLink,
+ useCapabilities,
+} from '../../../../hooks';
+import { WithHeaderLayout } from '../../../../layouts';
import { useSetPackageInstallStatus } from '../../hooks';
+import { IconPanel, LoadingIconPanel } from '../../components/icon_panel';
+import { RELEASE_BADGE_LABEL, RELEASE_BADGE_DESCRIPTION } from '../../components/release_badge';
+import { UpdateIcon } from '../../components/icons';
import { Content } from './content';
-import { Header } from './header';
export const DEFAULT_PANEL: DetailViewPanelName = 'overview';
@@ -20,66 +42,202 @@ export interface DetailParams {
panel?: DetailViewPanelName;
}
+const Divider = styled.div`
+ width: 0;
+ height: 100%;
+ border-left: ${(props) => props.theme.eui.euiBorderThin};
+`;
+
+// Allows child text to be truncated
+const FlexItemWithMinWidth = styled(EuiFlexItem)`
+ min-width: 0px;
+`;
+
+function Breadcrumbs({ packageTitle }: { packageTitle: string }) {
+ useBreadcrumbs('integration_details', { pkgTitle: packageTitle });
+ return null;
+}
+
export function Detail() {
// TODO: fix forced cast if possible
const { pkgkey, panel = DEFAULT_PANEL } = useParams() as DetailParams;
+ const { getHref } = useLink();
+ const hasWriteCapabilites = useCapabilities().write;
- const [info, setInfo] = useState(null);
+ // Package info state
+ const [packageInfo, setPackageInfo] = useState(null);
const setPackageInstallStatus = useSetPackageInstallStatus();
+ const updateAvailable =
+ packageInfo &&
+ 'savedObject' in packageInfo &&
+ packageInfo.savedObject &&
+ packageInfo.savedObject.attributes.version < packageInfo.latestVersion;
+
+ // Fetch package info
+ const { data: packageInfoData, error: packageInfoError, isLoading } = useGetPackageInfoByKey(
+ pkgkey
+ );
+
+ // Track install status state
useEffect(() => {
- sendGetPackageInfoByKey(pkgkey).then((response) => {
- const packageInfo = response.data?.response;
- const title = packageInfo?.title;
- const name = packageInfo?.name;
+ if (packageInfoData?.response) {
+ const packageInfoResponse = packageInfoData.response;
+ setPackageInfo(packageInfoResponse);
+
let installedVersion;
- if (packageInfo && 'savedObject' in packageInfo) {
- installedVersion = packageInfo.savedObject.attributes.version;
+ const { name } = packageInfoData.response;
+ if ('savedObject' in packageInfoResponse) {
+ installedVersion = packageInfoResponse.savedObject.attributes.version;
}
- const status: InstallStatus = packageInfo?.status as any;
-
- // track install status state
+ const status: InstallStatus = packageInfoResponse?.status as any;
if (name) {
setPackageInstallStatus({ name, status, version: installedVersion || null });
}
- if (packageInfo) {
- setInfo({ ...packageInfo, title: title || '' });
- }
- });
- }, [pkgkey, setPackageInstallStatus]);
-
- if (!info) return null;
-
- return ;
-}
+ }
+ }, [packageInfoData, setPackageInstallStatus, setPackageInfo]);
-const FullWidthHeader = styled(EuiPage)`
- border-bottom: ${(props) => props.theme.eui.euiBorderThin};
- padding-bottom: ${(props) => props.theme.eui.paddingSizes.xl};
-`;
+ const headerLeftContent = useMemo(
+ () => (
+
+
+ {/* Allows button to break out of full width */}
+
+
+
+
+
+
+
+
+
+ {isLoading || !packageInfo ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {/* Render space in place of package name while package info loads to prevent layout from jumping around */}
+ {packageInfo?.title || '\u00A0'}
+
+
+ {packageInfo?.release && packageInfo.release !== 'ga' ? (
+
+
+
+ ) : null}
+
+
+
+
+
+ ),
+ [getHref, isLoading, packageInfo]
+ );
-const FullWidthContent = styled(EuiPage)`
- background-color: ${(props) => props.theme.eui.euiColorEmptyShade};
- padding-top: ${(props) => parseInt(props.theme.eui.paddingSizes.xl, 10) * 1.25}px;
- flex-grow: 1;
-`;
+ const headerRightContent = useMemo(
+ () =>
+ packageInfo ? (
+ <>
+
+
+ {[
+ {
+ label: i18n.translate('xpack.ingestManager.epm.versionLabel', {
+ defaultMessage: 'Version',
+ }),
+ content: (
+
+ {packageInfo.version}
+ {updateAvailable ? (
+
+
+
+ ) : null}
+
+ ),
+ },
+ { isDivider: true },
+ {
+ content: (
+
+
+
+ ),
+ },
+ ].map((item, index) => (
+
+ {item.isDivider ?? false ? (
+
+ ) : item.label ? (
+
+ {item.label}
+ {item.content}
+
+ ) : (
+ item.content
+ )}
+
+ ))}
+
+ >
+ ) : undefined,
+ [getHref, hasWriteCapabilites, packageInfo, pkgkey, updateAvailable]
+ );
-type LayoutProps = PackageInfo & Pick & Pick;
-export function DetailLayout(props: LayoutProps) {
- const { name: packageName, version, icons, restrictWidth, title: packageTitle } = props;
- const iconType = usePackageIconType({ packageName, version, icons });
- useBreadcrumbs('integration_details', { pkgTitle: packageTitle });
return (
-
-
-
-
-
-
-
-
-
-
-
-
+
+ {packageInfo ? : null}
+ {packageInfoError ? (
+
+ }
+ error={packageInfoError}
+ />
+ ) : isLoading || !packageInfo ? (
+
+ ) : (
+
+ )}
+
);
}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/layout.tsx
index a802e35add7db..c329596384730 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/layout.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/layout.tsx
@@ -22,7 +22,7 @@ export const LeftColumn: FunctionComponent = ({ children, ...rest }
export const CenterColumn: FunctionComponent = ({ children, ...rest }) => {
return (
-
+
{children}
);
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/screenshots.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/screenshots.tsx
index 696af14604c5b..d8388a71556d6 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/screenshots.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/screenshots.tsx
@@ -3,9 +3,10 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import React, { Fragment } from 'react';
import styled from 'styled-components';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiFlexGroup, EuiFlexItem, EuiImage, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui';
import { ScreenshotItem } from '../../../../types';
import { useLinks } from '../../hooks';
@@ -13,6 +14,29 @@ interface ScreenshotProps {
images: ScreenshotItem[];
}
+const getHorizontalPadding = (styledProps: any): number =>
+ parseInt(styledProps.theme.eui.paddingSizes.xl, 10) * 2;
+const getVerticalPadding = (styledProps: any): number =>
+ parseInt(styledProps.theme.eui.paddingSizes.xl, 10) * 1.75;
+const getPadding = (styledProps: any) =>
+ styledProps.hascaption
+ ? `${styledProps.theme.eui.paddingSizes.xl} ${getHorizontalPadding(
+ styledProps
+ )}px ${getVerticalPadding(styledProps)}px`
+ : `${getHorizontalPadding(styledProps)}px ${getVerticalPadding(styledProps)}px`;
+const ScreenshotsContainer = styled(EuiFlexGroup)`
+ background: linear-gradient(360deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0) 100%),
+ ${(styledProps) => styledProps.theme.eui.euiColorPrimary};
+ padding: ${(styledProps) => getPadding(styledProps)};
+ flex: 0 0 auto;
+ border-radius: ${(styledProps) => styledProps.theme.eui.euiBorderRadius};
+`;
+
+// fixes ie11 problems with nested flex items
+const NestedEuiFlexItem = styled(EuiFlexItem)`
+ flex: 0 0 auto !important;
+`;
+
export function Screenshots(props: ScreenshotProps) {
const { toImage } = useLinks();
const { images } = props;
@@ -21,36 +45,23 @@ export function Screenshots(props: ScreenshotProps) {
const image = images[0];
const hasCaption = image.title ? true : false;
- const getHorizontalPadding = (styledProps: any): number =>
- parseInt(styledProps.theme.eui.paddingSizes.xl, 10) * 2;
- const getVerticalPadding = (styledProps: any): number =>
- parseInt(styledProps.theme.eui.paddingSizes.xl, 10) * 1.75;
- const getPadding = (styledProps: any) =>
- hasCaption
- ? `${styledProps.theme.eui.paddingSizes.xl} ${getHorizontalPadding(
- styledProps
- )}px ${getVerticalPadding(styledProps)}px`
- : `${getHorizontalPadding(styledProps)}px ${getVerticalPadding(styledProps)}px`;
-
- const ScreenshotsContainer = styled(EuiFlexGroup)`
- background: linear-gradient(360deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0) 100%),
- ${(styledProps) => styledProps.theme.eui.euiColorPrimary};
- padding: ${(styledProps) => getPadding(styledProps)};
- flex: 0 0 auto;
- border-radius: ${(styledProps) => styledProps.theme.eui.euiBorderRadius};
- `;
-
- // fixes ie11 problems with nested flex items
- const NestedEuiFlexItem = styled(EuiFlexItem)`
- flex: 0 0 auto !important;
- `;
return (
- Screenshots
+
+
+
-
+
{hasCaption && (
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx
index 125289ce3ee8d..4832a89479026 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/settings_panel.tsx
@@ -33,7 +33,7 @@ const NoteLabel = () => (
);
const UpdatesAvailableMsg = () => (
-
+
{entries(PanelDisplayNames).map(([panel, display]) => {
- const Link = styled(EuiButtonEmpty).attrs({
- href: getHref('integration_details', { pkgkey: `${name}-${version}`, panel }),
- })`
- font-weight: ${(p) =>
- active === panel
- ? p.theme.eui.euiFontWeightSemiBold
- : p.theme.eui.euiFontWeightRegular};
- `;
// Don't display usages tab as we haven't implemented this yet
// FIXME: Restore when we implement usages page
if (panel === 'usages' && (true || packageInstallStatus.status !== InstallStatus.installed))
@@ -50,7 +41,11 @@ export function SideNavLinks({ name, version, active }: NavLinkProps) {
return (
- {display}
+
+ {active === panel ? {display} : display}
+
);
})}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/header.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/header.tsx
index c378e5a47a9b9..363b1ede89e9e 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/header.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/header.tsx
@@ -39,22 +39,26 @@ export const HeroCopy = memo(() => {
);
});
+const Illustration = styled(EuiImage)`
+ margin-bottom: -68px;
+ width: 80%;
+`;
+
export const HeroImage = memo(() => {
const { toAssets } = useLinks();
const { uiSettings } = useCore();
const IS_DARK_THEME = uiSettings.get('theme:darkMode');
- const Illustration = styled(EuiImage).attrs((props) => ({
- alt: i18n.translate('xpack.ingestManager.epm.illustrationAltText', {
- defaultMessage: 'Illustration of an integration',
- }),
- url: IS_DARK_THEME
- ? toAssets('illustration_integrations_darkmode.svg')
- : toAssets('illustration_integrations_lightmode.svg'),
- }))`
- margin-bottom: -68px;
- width: 80%;
- `;
-
- return ;
+ return (
+
+ );
});
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx
index c68833c1b2d95..a8e4d0105066b 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/index.tsx
@@ -61,7 +61,9 @@ export function EPMHomePage() {
function InstalledPackages() {
useBreadcrumbs('integrations_installed');
- const { data: allPackages, isLoading: isLoadingPackages } = useGetPackages();
+ const { data: allPackages, isLoading: isLoadingPackages } = useGetPackages({
+ experimental: true,
+ });
const [selectedCategory, setSelectedCategory] = useState('');
const title = i18n.translate('xpack.ingestManager.epmList.installedTitle', {
@@ -118,7 +120,8 @@ function AvailablePackages() {
const queryParams = new URLSearchParams(useLocation().search);
const initialCategory = queryParams.get('category') || '';
const [selectedCategory, setSelectedCategory] = useState(initialCategory);
- const { data: categoryPackagesRes, isLoading: isLoadingPackages } = useGetPackages({
+ const { data: allPackagesRes, isLoading: isLoadingAllPackages } = useGetPackages();
+ const { data: categoryPackagesRes, isLoading: isLoadingCategoryPackages } = useGetPackages({
category: selectedCategory,
});
const { data: categoriesRes, isLoading: isLoadingCategories } = useGetCategories();
@@ -126,7 +129,7 @@ function AvailablePackages() {
categoryPackagesRes && categoryPackagesRes.response ? categoryPackagesRes.response : [];
const title = i18n.translate('xpack.ingestManager.epmList.allTitle', {
- defaultMessage: 'All integrations',
+ defaultMessage: 'Browse by category',
});
const categories = [
@@ -135,13 +138,13 @@ function AvailablePackages() {
title: i18n.translate('xpack.ingestManager.epmList.allPackagesFilterLinkText', {
defaultMessage: 'All',
}),
- count: packages.length,
+ count: allPackagesRes?.response?.length || 0,
},
...(categoriesRes ? categoriesRes.response : []),
];
const controls = categories ? (
{
@@ -156,7 +159,7 @@ function AvailablePackages() {
return (
;
- allPackages: PackageList;
-}
-
-export function SearchPackages({ searchTerm, localSearchRef, allPackages }: SearchPackagesProps) {
- // this means the search index hasn't been built yet.
- // i.e. the intial fetch of all packages hasn't finished
- if (!localSearchRef.current) return Still fetching matches. Try again in a moment.
;
-
- const matches = localSearchRef.current.search(searchTerm) as PackageList;
- const matchingIds = matches.map((match) => match[searchIdField]);
- const filtered = allPackages.filter((item) => matchingIds.includes(item[searchIdField]));
-
- return ;
-}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/search_results.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/search_results.tsx
deleted file mode 100644
index fbdcaac01931b..0000000000000
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/home/search_results.tsx
+++ /dev/null
@@ -1,33 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { EuiText, EuiTitle } from '@elastic/eui';
-import React from 'react';
-import { PackageList } from '../../../../types';
-import { PackageListGrid } from '../../components/package_list_grid';
-
-interface SearchResultsProps {
- term: string;
- results: PackageList;
-}
-
-export function SearchResults({ term, results }: SearchResultsProps) {
- const title = 'Search results';
- return (
-
-
- {results.length} results for "{term}"
-
-
- }
- />
- );
-}
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx
index ec58789becb72..36a8bf908ddd7 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx
@@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
-import React, { useState } from 'react';
+import React, { useState, useMemo } from 'react';
import {
EuiBasicTable,
EuiButton,
@@ -25,7 +25,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react';
import { CSSProperties } from 'styled-components';
import { AgentEnrollmentFlyout } from '../components';
-import { Agent } from '../../../types';
+import { Agent, AgentConfig } from '../../../types';
import {
usePagination,
useCapabilities,
@@ -178,11 +178,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
}
if (selectedStatus.length) {
- if (kuery) {
- kuery = `(${kuery}) and`;
- }
-
- kuery = selectedStatus
+ const kueryStatus = selectedStatus
.map((status) => {
switch (status) {
case 'online':
@@ -196,6 +192,12 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
return '';
})
.join(' or ');
+
+ if (kuery) {
+ kuery = `(${kuery}) and ${kueryStatus}`;
+ } else {
+ kuery = kueryStatus;
+ }
}
const agentsRequest = useGetAgents(
@@ -220,6 +222,13 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
});
const agentConfigs = agentConfigsRequest.data ? agentConfigsRequest.data.items : [];
+ const agentConfigsIndexedById = useMemo(() => {
+ return agentConfigs.reduce((acc, config) => {
+ acc[config.id] = config;
+
+ return acc;
+ }, {} as { [k: string]: AgentConfig });
+ }, [agentConfigs]);
const { isLoading: isAgentConfigsLoading } = agentConfigsRequest;
const columns = [
@@ -271,9 +280,10 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
)}
- {agent.config_revision &&
- agent.config_newest_revision &&
- agent.config_newest_revision > agent.config_revision && (
+ {agent.config_id &&
+ agent.config_revision &&
+ agentConfigsIndexedById[agent.config_id] &&
+ agentConfigsIndexedById[agent.config_id].revision > agent.config_revision && (
diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_health.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_health.tsx
index e4dfa520259eb..7c6c95cab420f 100644
--- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_health.tsx
+++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_health.tsx
@@ -53,6 +53,22 @@ const Status = {
/>
),
+ Degraded: (
+
+
+
+ ),
+ Enrolling: (
+
+
+
+ ),
Unenrolling: (
{
+export const getCategoriesHandler: RequestHandler<
+ undefined,
+ TypeOf
+> = async (context, request, response) => {
try {
- const res = await getCategories();
+ const res = await getCategories(request.query);
const body: GetCategoriesResponse = {
response: res,
success: true,
@@ -54,7 +58,7 @@ export const getListHandler: RequestHandler<
const savedObjectsClient = context.core.savedObjects.client;
const res = await getPackages({
savedObjectsClient,
- category: request.query.category,
+ ...request.query,
});
const body: GetPackagesResponse = {
response: res,
diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/index.ts b/x-pack/plugins/ingest_manager/server/routes/epm/index.ts
index ffaf0ce46c89a..b524a7b33923e 100644
--- a/x-pack/plugins/ingest_manager/server/routes/epm/index.ts
+++ b/x-pack/plugins/ingest_manager/server/routes/epm/index.ts
@@ -15,6 +15,7 @@ import {
deletePackageHandler,
} from './handlers';
import {
+ GetCategoriesRequestSchema,
GetPackagesRequestSchema,
GetFileRequestSchema,
GetInfoRequestSchema,
@@ -26,7 +27,7 @@ export const registerRoutes = (router: IRouter) => {
router.get(
{
path: EPM_API_ROUTES.CATEGORIES_PATTERN,
- validate: false,
+ validate: GetCategoriesRequestSchema,
options: { tags: [`access:${PLUGIN_ID}-read`] },
},
getCategoriesHandler
diff --git a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts
index b47cf4f7e7c3b..6c360fdeda460 100644
--- a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts
+++ b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts
@@ -38,6 +38,7 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = {
package_auto_upgrade: { type: 'keyword' },
kibana_url: { type: 'keyword' },
kibana_ca_sha256: { type: 'keyword' },
+ has_seen_add_data_notice: { type: 'boolean', index: false },
},
},
},
@@ -63,8 +64,8 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = {
config_id: { type: 'keyword' },
last_updated: { type: 'date' },
last_checkin: { type: 'date' },
+ last_checkin_status: { type: 'keyword' },
config_revision: { type: 'integer' },
- config_newest_revision: { type: 'integer' },
default_api_key_id: { type: 'keyword' },
default_api_key: { type: 'binary', index: false },
updated_at: { type: 'date' },
@@ -311,6 +312,7 @@ export function registerEncryptedSavedObjects(
'config_id',
'last_updated',
'last_checkin',
+ 'last_checkin_status',
'config_revision',
'config_newest_revision',
'updated_at',
diff --git a/x-pack/plugins/ingest_manager/server/services/agent_config_update.ts b/x-pack/plugins/ingest_manager/server/services/agent_config_update.ts
index 1cca165906732..3d40d128afda8 100644
--- a/x-pack/plugins/ingest_manager/server/services/agent_config_update.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agent_config_update.ts
@@ -6,7 +6,7 @@
import { SavedObjectsClientContract } from 'src/core/server';
import { generateEnrollmentAPIKey, deleteEnrollmentApiKeyForConfigId } from './api_keys';
-import { updateAgentsForConfigId, unenrollForConfigId } from './agents';
+import { unenrollForConfigId } from './agents';
import { outputService } from './output';
export async function agentConfigUpdateEventHandler(
@@ -26,10 +26,6 @@ export async function agentConfigUpdateEventHandler(
});
}
- if (action === 'updated') {
- await updateAgentsForConfigId(soClient, configId);
- }
-
if (action === 'deleted') {
await unenrollForConfigId(soClient, configId);
await deleteEnrollmentApiKeyForConfigId(soClient, configId);
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/index.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/index.ts
index 7c6641bbb5faa..ece38f86b4987 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/index.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin/index.ts
@@ -11,7 +11,6 @@ import {
AgentEvent,
AgentSOAttributes,
AgentEventSOAttributes,
- AgentMetadata,
} from '../../../types';
import { AGENT_SAVED_OBJECT_TYPE, AGENT_EVENT_SAVED_OBJECT_TYPE } from '../../../constants';
@@ -21,20 +20,24 @@ import { getAgentActionsForCheckin } from '../actions';
export async function agentCheckin(
soClient: SavedObjectsClientContract,
agent: Agent,
- events: NewAgentEvent[],
- localMetadata?: any,
+ data: {
+ events: NewAgentEvent[];
+ localMetadata?: any;
+ status?: 'online' | 'error' | 'degraded';
+ },
options?: { signal: AbortSignal }
) {
- const updateData: {
- local_metadata?: AgentMetadata;
- current_error_events?: string;
- } = {};
- const { updatedErrorEvents } = await processEventsForCheckin(soClient, agent, events);
+ const updateData: Partial = {};
+ const { updatedErrorEvents } = await processEventsForCheckin(soClient, agent, data.events);
if (updatedErrorEvents) {
updateData.current_error_events = JSON.stringify(updatedErrorEvents);
}
- if (localMetadata) {
- updateData.local_metadata = localMetadata;
+ if (data.localMetadata) {
+ updateData.local_metadata = data.localMetadata;
+ }
+
+ if (data.status !== agent.last_checkin_status) {
+ updateData.last_checkin_status = data.status;
}
if (Object.keys(updateData).length > 0) {
await soClient.update(AGENT_SAVED_OBJECT_TYPE, agent.id, updateData);
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_connected_agents.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_connected_agents.ts
index 96e006b78f00f..994ecc64c82a7 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_connected_agents.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_connected_agents.ts
@@ -59,7 +59,7 @@ export function agentCheckinStateConnectedAgentsFactory() {
const internalSOClient = getInternalUserSOClient();
const now = new Date().toISOString();
const updates: Array> = [
- ...connectedAgentsIds.values(),
+ ...agentToUpdate.values(),
].map((agentId) => ({
type: AGENT_SAVED_OBJECT_TYPE,
id: agentId,
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/reassign.ts b/x-pack/plugins/ingest_manager/server/services/agents/reassign.ts
index f8142af376eb3..ecc2c987d04b6 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/reassign.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/reassign.ts
@@ -23,6 +23,5 @@ export async function reassignAgent(
await soClient.update(AGENT_SAVED_OBJECT_TYPE, agentId, {
config_id: newConfigId,
config_revision: null,
- config_newest_revision: config.revision,
});
}
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts
index 8140b1e6de470..f216cd541eb21 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/status.test.ts
@@ -33,6 +33,7 @@ describe('Agent status service', () => {
type: AGENT_TYPE_PERMANENT,
attributes: {
active: true,
+ last_checkin: new Date().toISOString(),
local_metadata: {},
user_provided_metadata: {},
},
@@ -40,4 +41,36 @@ describe('Agent status service', () => {
const status = await getAgentStatusById(mockSavedObjectsClient, 'id');
expect(status).toEqual('online');
});
+
+ it('should return enrolling when agent is active but never checkin', async () => {
+ const mockSavedObjectsClient = savedObjectsClientMock.create();
+ mockSavedObjectsClient.get = jest.fn().mockReturnValue({
+ id: 'id',
+ type: AGENT_TYPE_PERMANENT,
+ attributes: {
+ active: true,
+ local_metadata: {},
+ user_provided_metadata: {},
+ },
+ } as SavedObject);
+ const status = await getAgentStatusById(mockSavedObjectsClient, 'id');
+ expect(status).toEqual('enrolling');
+ });
+
+ it('should return unenrolling when agent is unenrolling', async () => {
+ const mockSavedObjectsClient = savedObjectsClientMock.create();
+ mockSavedObjectsClient.get = jest.fn().mockReturnValue({
+ id: 'id',
+ type: AGENT_TYPE_PERMANENT,
+ attributes: {
+ active: true,
+ last_checkin: new Date().toISOString(),
+ unenrollment_started_at: new Date().toISOString(),
+ local_metadata: {},
+ user_provided_metadata: {},
+ },
+ } as SavedObject);
+ const status = await getAgentStatusById(mockSavedObjectsClient, 'id');
+ expect(status).toEqual('unenrolling');
+ });
});
diff --git a/x-pack/plugins/ingest_manager/server/services/agents/update.ts b/x-pack/plugins/ingest_manager/server/services/agents/update.ts
index ec7a42ff11b7a..11ad76fe81784 100644
--- a/x-pack/plugins/ingest_manager/server/services/agents/update.ts
+++ b/x-pack/plugins/ingest_manager/server/services/agents/update.ts
@@ -8,38 +8,6 @@ import { SavedObjectsClientContract } from 'src/core/server';
import { listAgents } from './crud';
import { AGENT_SAVED_OBJECT_TYPE } from '../../constants';
import { unenrollAgent } from './unenroll';
-import { agentConfigService } from '../agent_config';
-
-export async function updateAgentsForConfigId(
- soClient: SavedObjectsClientContract,
- configId: string
-) {
- const config = await agentConfigService.get(soClient, configId);
- if (!config) {
- throw new Error('Config not found');
- }
- let hasMore = true;
- let page = 1;
- while (hasMore) {
- const { agents } = await listAgents(soClient, {
- kuery: `${AGENT_SAVED_OBJECT_TYPE}.config_id:"${configId}"`,
- page: page++,
- perPage: 1000,
- showInactive: true,
- });
- if (agents.length === 0) {
- hasMore = false;
- break;
- }
- const agentUpdate = agents.map((agent) => ({
- id: agent.id,
- type: AGENT_SAVED_OBJECT_TYPE,
- attributes: { config_newest_revision: config.revision },
- }));
-
- await soClient.bulkUpdate(agentUpdate);
- }
-}
export async function unenrollForConfigId(soClient: SavedObjectsClientContract, configId: string) {
let hasMore = true;
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap
index 848e65b7931eb..7437321163749 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/__snapshots__/template.test.ts.snap
@@ -99,7 +99,8 @@ exports[`tests loading base.yml: base.yml 1`] = `
"package": {
"name": "nginx"
},
- "managed_by": "ingest-manager"
+ "managed_by": "ingest-manager",
+ "managed": true
}
}
`;
@@ -203,7 +204,8 @@ exports[`tests loading coredns.logs.yml: coredns.logs.yml 1`] = `
"package": {
"name": "coredns"
},
- "managed_by": "ingest-manager"
+ "managed_by": "ingest-manager",
+ "managed": true
}
}
`;
@@ -1691,7 +1693,8 @@ exports[`tests loading system.yml: system.yml 1`] = `
"package": {
"name": "system"
},
- "managed_by": "ingest-manager"
+ "managed_by": "ingest-manager",
+ "managed": true
}
}
`;
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts
index e7867532ed176..77ad96952269f 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/template/template.ts
@@ -317,6 +317,7 @@ function getBaseTemplate(
name: packageName,
},
managed_by: 'ingest-manager',
+ managed: true,
},
};
}
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts
deleted file mode 100644
index ae6493d4716e8..0000000000000
--- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/install.ts
+++ /dev/null
@@ -1,119 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import {
- SavedObject,
- SavedObjectsBulkCreateObject,
- SavedObjectsClientContract,
-} from 'src/core/server';
-import * as Registry from '../../registry';
-import { AssetType, KibanaAssetType, AssetReference } from '../../../../types';
-
-type SavedObjectToBe = Required & { type: AssetType };
-export type ArchiveAsset = Pick<
- SavedObject,
- 'id' | 'attributes' | 'migrationVersion' | 'references'
-> & {
- type: AssetType;
-};
-
-export async function getKibanaAsset(key: string) {
- const buffer = Registry.getAsset(key);
-
- // cache values are buffers. convert to string / JSON
- return JSON.parse(buffer.toString('utf8'));
-}
-
-export function createSavedObjectKibanaAsset(
- jsonAsset: ArchiveAsset,
- pkgName: string
-): SavedObjectToBe {
- // convert that to an object
- const asset = changeAssetIds(jsonAsset, pkgName);
-
- return {
- type: asset.type,
- id: asset.id,
- attributes: asset.attributes,
- references: asset.references || [],
- migrationVersion: asset.migrationVersion || {},
- };
-}
-
-// modifies id property and the id property of references objects (not index-pattern)
-// to be prepended with the package name to distinguish assets from Beats modules' assets
-export const changeAssetIds = (asset: ArchiveAsset, pkgName: string): ArchiveAsset => {
- const references = asset.references.map((ref) => {
- if (ref.type === KibanaAssetType.indexPattern) return ref;
- const id = getAssetId(ref.id, pkgName);
- return { ...ref, id };
- });
- return {
- ...asset,
- id: getAssetId(asset.id, pkgName),
- references,
- };
-};
-
-export const getAssetId = (id: string, pkgName: string) => {
- return `${pkgName}-${id}`;
-};
-
-// TODO: make it an exhaustive list
-// e.g. switch statement with cases for each enum key returning `never` for default case
-export async function installKibanaAssets(options: {
- savedObjectsClient: SavedObjectsClientContract;
- pkgName: string;
- paths: string[];
-}) {
- const { savedObjectsClient, paths, pkgName } = options;
-
- // Only install Kibana assets during package installation.
- const kibanaAssetTypes = Object.values(KibanaAssetType);
- const installationPromises = kibanaAssetTypes.map((assetType) =>
- installKibanaSavedObjects({ savedObjectsClient, assetType, paths, pkgName })
- );
-
- // installKibanaSavedObjects returns AssetReference[], so .map creates AssetReference[][]
- // call .flat to flatten into one dimensional array
- return Promise.all(installationPromises).then((results) => results.flat());
-}
-
-async function installKibanaSavedObjects({
- savedObjectsClient,
- assetType,
- paths,
- pkgName,
-}: {
- savedObjectsClient: SavedObjectsClientContract;
- assetType: KibanaAssetType;
- paths: string[];
- pkgName: string;
-}) {
- const isSameType = (path: string) => assetType === Registry.pathParts(path).type;
- const pathsOfType = paths.filter((path) => isSameType(path));
- const kibanaAssets = await Promise.all(pathsOfType.map((path) => getKibanaAsset(path)));
- const toBeSavedObjects = await Promise.all(
- kibanaAssets.map((asset) => createSavedObjectKibanaAsset(asset, pkgName))
- );
-
- if (toBeSavedObjects.length === 0) {
- return [];
- } else {
- const createResults = await savedObjectsClient.bulkCreate(toBeSavedObjects, {
- overwrite: true,
- });
- const createdObjects = createResults.saved_objects;
- const installed = createdObjects.map(toAssetReference);
- return installed;
- }
-}
-
-function toAssetReference({ id, type }: SavedObject) {
- const reference: AssetReference = { id, type: type as KibanaAssetType };
-
- return reference;
-}
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/__snapshots__/install.test.ts.snap b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/__snapshots__/install.test.ts.snap
deleted file mode 100644
index 638ed4b6118c9..0000000000000
--- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/__snapshots__/install.test.ts.snap
+++ /dev/null
@@ -1,133 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`a kibana asset id and its reference ids are appended with package name changeAssetIds output matches snapshot: dashboard.json 1`] = `
-{
- "attributes": {
- "description": "Overview dashboard for the Nginx integration in Metrics",
- "hits": 0,
- "kibanaSavedObjectMeta": {
- "searchSourceJSON": {
- "filter": [],
- "highlightAll": true,
- "query": {
- "language": "kuery",
- "query": ""
- },
- "version": true
- }
- },
- "optionsJSON": {
- "darkTheme": false,
- "hidePanelTitles": false,
- "useMargins": true
- },
- "panelsJSON": [
- {
- "embeddableConfig": {},
- "gridData": {
- "h": 12,
- "i": "1",
- "w": 24,
- "x": 24,
- "y": 0
- },
- "panelIndex": "1",
- "panelRefName": "panel_0",
- "version": "7.3.0"
- },
- {
- "embeddableConfig": {},
- "gridData": {
- "h": 12,
- "i": "2",
- "w": 24,
- "x": 24,
- "y": 12
- },
- "panelIndex": "2",
- "panelRefName": "panel_1",
- "version": "7.3.0"
- },
- {
- "embeddableConfig": {},
- "gridData": {
- "h": 12,
- "i": "3",
- "w": 24,
- "x": 0,
- "y": 12
- },
- "panelIndex": "3",
- "panelRefName": "panel_2",
- "version": "7.3.0"
- },
- {
- "embeddableConfig": {},
- "gridData": {
- "h": 12,
- "i": "4",
- "w": 24,
- "x": 0,
- "y": 0
- },
- "panelIndex": "4",
- "panelRefName": "panel_3",
- "version": "7.3.0"
- },
- {
- "embeddableConfig": {},
- "gridData": {
- "h": 12,
- "i": "5",
- "w": 48,
- "x": 0,
- "y": 24
- },
- "panelIndex": "5",
- "panelRefName": "panel_4",
- "version": "7.3.0"
- }
- ],
- "timeRestore": false,
- "title": "[Metrics Nginx] Overview ECS",
- "version": 1
- },
- "id": "nginx-023d2930-f1a5-11e7-a9ef-93c69af7b129-ecs",
- "migrationVersion": {
- "dashboard": "7.3.0"
- },
- "references": [
- {
- "id": "metrics-*",
- "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index",
- "type": "index-pattern"
- },
- {
- "id": "nginx-555df8a0-f1a1-11e7-a9ef-93c69af7b129-ecs",
- "name": "panel_0",
- "type": "search"
- },
- {
- "id": "nginx-a1d92240-f1a1-11e7-a9ef-93c69af7b129-ecs",
- "name": "panel_1",
- "type": "map"
- },
- {
- "id": "nginx-d763a570-f1a1-11e7-a9ef-93c69af7b129-ecs",
- "name": "panel_2",
- "type": "dashboard"
- },
- {
- "id": "nginx-47a8e0f0-f1a4-11e7-a9ef-93c69af7b129-ecs",
- "name": "panel_3",
- "type": "visualization"
- },
- {
- "id": "nginx-dcbffe30-f1a4-11e7-a9ef-93c69af7b129-ecs",
- "name": "panel_4",
- "type": "visualization"
- }
- ],
- "type": "dashboard"
-}
-`;
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/dashboard.json b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/dashboard.json
deleted file mode 100644
index e28a61ae5e18c..0000000000000
--- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/dashboard.json
+++ /dev/null
@@ -1,129 +0,0 @@
-{
- "attributes": {
- "description": "Overview dashboard for the Nginx integration in Metrics",
- "hits": 0,
- "kibanaSavedObjectMeta": {
- "searchSourceJSON": {
- "filter": [],
- "highlightAll": true,
- "query": {
- "language": "kuery",
- "query": ""
- },
- "version": true
- }
- },
- "optionsJSON": {
- "darkTheme": false,
- "hidePanelTitles": false,
- "useMargins": true
- },
- "panelsJSON": [
- {
- "embeddableConfig": {},
- "gridData": {
- "h": 12,
- "i": "1",
- "w": 24,
- "x": 24,
- "y": 0
- },
- "panelIndex": "1",
- "panelRefName": "panel_0",
- "version": "7.3.0"
- },
- {
- "embeddableConfig": {},
- "gridData": {
- "h": 12,
- "i": "2",
- "w": 24,
- "x": 24,
- "y": 12
- },
- "panelIndex": "2",
- "panelRefName": "panel_1",
- "version": "7.3.0"
- },
- {
- "embeddableConfig": {},
- "gridData": {
- "h": 12,
- "i": "3",
- "w": 24,
- "x": 0,
- "y": 12
- },
- "panelIndex": "3",
- "panelRefName": "panel_2",
- "version": "7.3.0"
- },
- {
- "embeddableConfig": {},
- "gridData": {
- "h": 12,
- "i": "4",
- "w": 24,
- "x": 0,
- "y": 0
- },
- "panelIndex": "4",
- "panelRefName": "panel_3",
- "version": "7.3.0"
- },
- {
- "embeddableConfig": {},
- "gridData": {
- "h": 12,
- "i": "5",
- "w": 48,
- "x": 0,
- "y": 24
- },
- "panelIndex": "5",
- "panelRefName": "panel_4",
- "version": "7.3.0"
- }
- ],
- "timeRestore": false,
- "title": "[Metrics Nginx] Overview ECS",
- "version": 1
- },
- "id": "023d2930-f1a5-11e7-a9ef-93c69af7b129-ecs",
- "migrationVersion": {
- "dashboard": "7.3.0"
- },
- "references": [
- {
- "id": "metrics-*",
- "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index",
- "type": "index-pattern"
- },
- {
- "id": "555df8a0-f1a1-11e7-a9ef-93c69af7b129-ecs",
- "name": "panel_0",
- "type": "search"
- },
- {
- "id": "a1d92240-f1a1-11e7-a9ef-93c69af7b129-ecs",
- "name": "panel_1",
- "type": "map"
- },
- {
- "id": "d763a570-f1a1-11e7-a9ef-93c69af7b129-ecs",
- "name": "panel_2",
- "type": "dashboard"
- },
- {
- "id": "47a8e0f0-f1a4-11e7-a9ef-93c69af7b129-ecs",
- "name": "panel_3",
- "type": "visualization"
- },
- {
- "id": "dcbffe30-f1a4-11e7-a9ef-93c69af7b129-ecs",
- "name": "panel_4",
- "type": "visualization"
- }
- ],
- "type": "dashboard"
-}
\ No newline at end of file
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/install.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/install.test.ts
deleted file mode 100644
index f9bc4cdbf203f..0000000000000
--- a/x-pack/plugins/ingest_manager/server/services/epm/kibana/assets/tests/install.test.ts
+++ /dev/null
@@ -1,35 +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;
- * you may not use this file except in compliance with the Elastic License.
- */
-
-import { readFileSync } from 'fs';
-import path from 'path';
-import { getAssetId, changeAssetIds } from '../install';
-
-expect.addSnapshotSerializer({
- print(val) {
- return JSON.stringify(val, null, 2);
- },
-
- test(val) {
- return val;
- },
-});
-
-describe('a kibana asset id and its reference ids are appended with package name', () => {
- const assetPath = path.join(__dirname, './dashboard.json');
- const kibanaAsset = JSON.parse(readFileSync(assetPath, 'utf-8'));
- const pkgName = 'nginx';
- const modifiedAssetObject = changeAssetIds(kibanaAsset, pkgName);
-
- test('changeAssetIds output matches snapshot', () => {
- expect(modifiedAssetObject).toMatchSnapshot(path.basename(assetPath));
- });
-
- test('getAssetId', () => {
- const id = '47a8e0f0-f1a4-11e7-a9ef-93c69af7b129-ecs';
- expect(getAssetId(id, pkgName)).toBe(`${pkgName}-${id}`);
- });
-});
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts
index ad9635cc02e06..7093723806ea3 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get.ts
@@ -17,8 +17,8 @@ function nameAsTitle(name: string) {
return name.charAt(0).toUpperCase() + name.substr(1).toLowerCase();
}
-export async function getCategories() {
- return Registry.fetchCategories();
+export async function getCategories(options: Registry.CategoriesParams) {
+ return Registry.fetchCategories(options);
}
export async function getPackages(
@@ -26,8 +26,8 @@ export async function getPackages(
savedObjectsClient: SavedObjectsClientContract;
} & Registry.SearchParams
) {
- const { savedObjectsClient } = options;
- const registryItems = await Registry.fetchList({ category: options.category }).then((items) => {
+ const { savedObjectsClient, experimental, category } = options;
+ const registryItems = await Registry.fetchList({ category, experimental }).then((items) => {
return items.map((item) =>
Object.assign({}, item, { title: item.title || nameAsTitle(item.name) })
);
@@ -56,7 +56,7 @@ export async function getLimitedPackages(options: {
savedObjectsClient: SavedObjectsClientContract;
}): Promise {
const { savedObjectsClient } = options;
- const allPackages = await getPackages({ savedObjectsClient });
+ const allPackages = await getPackages({ savedObjectsClient, experimental: true });
const installedPackages = allPackages.filter(
(pkg) => (pkg.status = InstallationStatus.installed)
);
@@ -69,7 +69,7 @@ export async function getLimitedPackages(options: {
});
})
);
- return installedPackagesInfo.filter((pkgInfo) => isPackageLimited).map((pkgInfo) => pkgInfo.name);
+ return installedPackagesInfo.filter(isPackageLimited).map((pkgInfo) => pkgInfo.name);
}
export async function getPackageSavedObjects(savedObjectsClient: SavedObjectsClientContract) {
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.ts
new file mode 100644
index 0000000000000..b623295c5e060
--- /dev/null
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/get_objects.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { SavedObject, SavedObjectsBulkCreateObject } from 'src/core/server';
+import { AssetType } from '../../../types';
+import * as Registry from '../registry';
+
+type ArchiveAsset = Pick;
+type SavedObjectToBe = Required & { type: AssetType };
+
+export async function getObject(key: string) {
+ const buffer = Registry.getAsset(key);
+
+ // cache values are buffers. convert to string / JSON
+ const json = buffer.toString('utf8');
+ // convert that to an object
+ const asset: ArchiveAsset = JSON.parse(json);
+
+ const { type, file } = Registry.pathParts(key);
+ const savedObject: SavedObjectToBe = {
+ type,
+ id: file.replace('.json', ''),
+ attributes: asset.attributes,
+ references: asset.references || [],
+ migrationVersion: asset.migrationVersion || {},
+ };
+
+ return savedObject;
+}
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts
index 57c4f77432455..4bb803dfaf912 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/index.ts
@@ -23,7 +23,7 @@ export {
SearchParams,
} from './get';
-export { installPackage, ensureInstalledPackage } from './install';
+export { installKibanaAssets, installPackage, ensureInstalledPackage } from './install';
export { removeInstallation } from './remove';
type RequiredPackage = 'system' | 'endpoint';
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
index 8f73bc9a02765..910283549abdf 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
@@ -4,12 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { SavedObjectsClientContract } from 'src/core/server';
+import { SavedObject, SavedObjectsClientContract } from 'src/core/server';
import Boom from 'boom';
import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants';
import {
AssetReference,
Installation,
+ KibanaAssetType,
CallESAsCurrentUser,
DefaultPackages,
ElasticsearchAssetType,
@@ -17,7 +18,7 @@ import {
} from '../../../types';
import { installIndexPatterns } from '../kibana/index_pattern/install';
import * as Registry from '../registry';
-import { installKibanaAssets } from '../kibana/assets/install';
+import { getObject } from './get_objects';
import { getInstallation, getInstallationObject, isRequiredPackage } from './index';
import { installTemplates } from '../elasticsearch/template/install';
import { generateESIndexPatterns } from '../elasticsearch/template/template';
@@ -120,6 +121,7 @@ export async function installPackage(options: {
installKibanaAssets({
savedObjectsClient,
pkgName,
+ pkgVersion,
paths,
}),
installPipelines(registryPackageInfo, paths, callCluster),
@@ -183,6 +185,27 @@ export async function installPackage(options: {
});
}
+// TODO: make it an exhaustive list
+// e.g. switch statement with cases for each enum key returning `never` for default case
+export async function installKibanaAssets(options: {
+ savedObjectsClient: SavedObjectsClientContract;
+ pkgName: string;
+ pkgVersion: string;
+ paths: string[];
+}) {
+ const { savedObjectsClient, paths } = options;
+
+ // Only install Kibana assets during package installation.
+ const kibanaAssetTypes = Object.values(KibanaAssetType);
+ const installationPromises = kibanaAssetTypes.map(async (assetType) =>
+ installKibanaSavedObjects({ savedObjectsClient, assetType, paths })
+ );
+
+ // installKibanaSavedObjects returns AssetReference[], so .map creates AssetReference[][]
+ // call .flat to flatten into one dimensional array
+ return Promise.all(installationPromises).then((results) => results.flat());
+}
+
export async function saveInstallationReferences(options: {
savedObjectsClient: SavedObjectsClientContract;
pkgName: string;
@@ -217,3 +240,34 @@ export async function saveInstallationReferences(options: {
return toSaveAssetRefs;
}
+
+async function installKibanaSavedObjects({
+ savedObjectsClient,
+ assetType,
+ paths,
+}: {
+ savedObjectsClient: SavedObjectsClientContract;
+ assetType: KibanaAssetType;
+ paths: string[];
+}) {
+ const isSameType = (path: string) => assetType === Registry.pathParts(path).type;
+ const pathsOfType = paths.filter((path) => isSameType(path));
+ const toBeSavedObjects = await Promise.all(pathsOfType.map(getObject));
+
+ if (toBeSavedObjects.length === 0) {
+ return [];
+ } else {
+ const createResults = await savedObjectsClient.bulkCreate(toBeSavedObjects, {
+ overwrite: true,
+ });
+ const createdObjects = createResults.saved_objects;
+ const installed = createdObjects.map(toAssetReference);
+ return installed;
+ }
+}
+
+function toAssetReference({ id, type }: SavedObject) {
+ const reference: AssetReference = { id, type: type as KibanaAssetType };
+
+ return reference;
+}
diff --git a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts
index 0393cabca8ba2..ea906517f6dec 100644
--- a/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts
+++ b/x-pack/plugins/ingest_manager/server/services/epm/registry/index.ts
@@ -26,6 +26,11 @@ export { ArchiveEntry } from './extract';
export interface SearchParams {
category?: CategoryId;
+ experimental?: boolean;
+}
+
+export interface CategoriesParams {
+ experimental?: boolean;
}
export const pkgToPkgKey = ({ name, version }: { name: string; version: string }) =>
@@ -34,19 +39,23 @@ export const pkgToPkgKey = ({ name, version }: { name: string; version: string }
export async function fetchList(params?: SearchParams): Promise {
const registryUrl = getRegistryUrl();
const url = new URL(`${registryUrl}/search`);
- if (params && params.category) {
- url.searchParams.set('category', params.category);
+ if (params) {
+ if (params.category) {
+ url.searchParams.set('category', params.category);
+ }
+ if (params.experimental) {
+ url.searchParams.set('experimental', params.experimental.toString());
+ }
}
return fetchUrl(url.toString()).then(JSON.parse);
}
-export async function fetchFindLatestPackage(
- packageName: string,
- internal: boolean = true
-): Promise {
+export async function fetchFindLatestPackage(packageName: string): Promise {
const registryUrl = getRegistryUrl();
- const url = new URL(`${registryUrl}/search?package=${packageName}&internal=${internal}`);
+ const url = new URL(
+ `${registryUrl}/search?package=${packageName}&internal=true&experimental=true`
+ );
const res = await fetchUrl(url.toString());
const searchResults = JSON.parse(res);
if (searchResults.length) {
@@ -66,9 +75,16 @@ export async function fetchFile(filePath: string): Promise {
return getResponse(`${registryUrl}${filePath}`);
}
-export async function fetchCategories(): Promise {
+export async function fetchCategories(params?: CategoriesParams): Promise {
const registryUrl = getRegistryUrl();
- return fetchUrl(`${registryUrl}/categories`).then(JSON.parse);
+ const url = new URL(`${registryUrl}/categories`);
+ if (params) {
+ if (params.experimental) {
+ url.searchParams.set('experimental', params.experimental.toString());
+ }
+ }
+
+ return fetchUrl(url.toString()).then(JSON.parse);
}
export async function getArchiveInfo(
diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts
index a508c33e0347b..3e9209efcac04 100644
--- a/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts
+++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/agent.ts
@@ -32,6 +32,9 @@ export const PostAgentCheckinRequestSchema = {
agentId: schema.string(),
}),
body: schema.object({
+ status: schema.maybe(
+ schema.oneOf([schema.literal('online'), schema.literal('error'), schema.literal('degraded')])
+ ),
local_metadata: schema.maybe(schema.recordOf(schema.string(), schema.any())),
events: schema.maybe(schema.arrayOf(NewAgentEventSchema)),
}),
diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts
index 3ed6ee553a507..08f47a8f1caaa 100644
--- a/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts
+++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/epm.ts
@@ -5,9 +5,16 @@
*/
import { schema } from '@kbn/config-schema';
+export const GetCategoriesRequestSchema = {
+ query: schema.object({
+ experimental: schema.maybe(schema.boolean()),
+ }),
+};
+
export const GetPackagesRequestSchema = {
query: schema.object({
category: schema.maybe(schema.string()),
+ experimental: schema.maybe(schema.boolean()),
}),
};
diff --git a/x-pack/plugins/ingest_manager/server/types/rest_spec/settings.ts b/x-pack/plugins/ingest_manager/server/types/rest_spec/settings.ts
index f6e5fcbba7976..baee9f79d9317 100644
--- a/x-pack/plugins/ingest_manager/server/types/rest_spec/settings.ts
+++ b/x-pack/plugins/ingest_manager/server/types/rest_spec/settings.ts
@@ -13,5 +13,6 @@ export const PutSettingsRequestSchema = {
package_auto_upgrade: schema.maybe(schema.boolean()),
kibana_url: schema.maybe(schema.uri({ scheme: ['http', 'https'] })),
kibana_ca_sha256: schema.maybe(schema.string()),
+ has_seen_add_data_notice: schema.maybe(schema.boolean()),
}),
};
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 3e0b78d4f2e9d..8d6a83a625651 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
@@ -72,7 +72,7 @@ describe(' ', () => {
tableCellsValues.forEach((row, i) => {
const pipeline = pipelines[i];
- expect(row).toEqual(['', pipeline.name, '']);
+ expect(row).toEqual(['', pipeline.name, 'EditDelete']);
});
});
diff --git a/x-pack/plugins/ingest_pipelines/kibana.json b/x-pack/plugins/ingest_pipelines/kibana.json
index cb24133b1f6ba..75e5e9b5d6c51 100644
--- a/x-pack/plugins/ingest_pipelines/kibana.json
+++ b/x-pack/plugins/ingest_pipelines/kibana.json
@@ -5,5 +5,6 @@
"ui": true,
"requiredPlugins": ["licensing", "management"],
"optionalPlugins": ["security", "usageCollection"],
- "configPath": ["xpack", "ingest_pipelines"]
+ "configPath": ["xpack", "ingest_pipelines"],
+ "requiredBundles": ["esUiShared", "kibanaReact"]
}
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/inline_text_input.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/inline_text_input.tsx
index 00ac8d4f6d729..ea936115f1ac9 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/inline_text_input.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/pipeline_processors_editor_item/inline_text_input.tsx
@@ -5,7 +5,7 @@
*/
import classNames from 'classnames';
import React, { FunctionComponent, useState, useEffect, useCallback } from 'react';
-import { EuiFieldText, EuiText, keyCodes } from '@elastic/eui';
+import { EuiFieldText, EuiText, keys } from '@elastic/eui';
export interface Props {
placeholder: string;
@@ -40,10 +40,10 @@ export const InlineTextInput: FunctionComponent = ({
useEffect(() => {
const keyboardListener = (event: KeyboardEvent) => {
- if (event.keyCode === keyCodes.ESCAPE || event.code === 'Escape') {
+ if (event.key === keys.ESCAPE || event.code === 'Escape') {
setIsShowingTextInput(false);
}
- if (event.keyCode === keyCodes.ENTER || event.code === 'Enter') {
+ if (event.key === keys.ENTER || event.code === 'Enter') {
submitChange();
}
};
diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.tsx
index db71cf25faacc..4458bd66c88de 100644
--- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.tsx
+++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.tsx
@@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FunctionComponent, memo, useRef, useEffect } from 'react';
-import { EuiFlexGroup, EuiFlexItem, keyCodes } from '@elastic/eui';
+import { EuiFlexGroup, EuiFlexItem, keys } from '@elastic/eui';
import { List, WindowScroller } from 'react-virtualized';
import { ProcessorInternal, ProcessorSelector } from '../../types';
@@ -52,7 +52,7 @@ export const ProcessorsTree: FunctionComponent = memo((props) => {
useEffect(() => {
const cancelMoveKbListener = (event: KeyboardEvent) => {
// x-browser support per https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode
- if (event.keyCode === keyCodes.ESCAPE || event.code === 'Escape') {
+ if (event.key === keys.ESCAPE || event.code === 'Escape') {
onAction({ type: 'cancelMove' });
}
};
diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json
index 7da5eaed5155e..b8747fc1f0cde 100644
--- a/x-pack/plugins/lens/kibana.json
+++ b/x-pack/plugins/lens/kibana.json
@@ -15,5 +15,6 @@
],
"optionalPlugins": ["embeddable", "usageCollection", "taskManager", "uiActions"],
"configPath": ["xpack", "lens"],
- "extraPublicDirs": ["common/constants"]
+ "extraPublicDirs": ["common/constants"],
+ "requiredBundles": ["savedObjects", "kibanaUtils", "kibanaReact", "embeddable"]
}
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx
index 0d60bd588f710..4a79f30a17a05 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx
@@ -324,8 +324,11 @@ describe('IndexPattern Data Panel', () => {
};
}
- async function testExistenceLoading(stateChanges?: unknown, propChanges?: unknown) {
- const props = testProps();
+ async function testExistenceLoading(
+ stateChanges?: unknown,
+ propChanges?: unknown,
+ props = testProps()
+ ) {
const inst = mountWithIntl( );
await act(async () => {
@@ -536,6 +539,25 @@ describe('IndexPattern Data Panel', () => {
expect(core.http.post).toHaveBeenCalledTimes(2);
expect(overlapCount).toEqual(0);
});
+
+ it("should default to empty dsl if query can't be parsed", async () => {
+ const props = {
+ ...testProps(),
+ query: {
+ language: 'kuery',
+ query: '@timestamp : NOT *',
+ },
+ };
+ await testExistenceLoading(undefined, undefined, props);
+
+ expect((props.core.http.post as jest.Mock).mock.calls[0][1].body).toContain(
+ JSON.stringify({
+ must_not: {
+ match_all: {},
+ },
+ })
+ );
+ });
});
describe('displaying field list', () => {
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx
index eb7940634d78e..91c068c2b4fab 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.tsx
@@ -22,7 +22,7 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
-import { DataPublicPluginStart } from 'src/plugins/data/public';
+import { DataPublicPluginStart, EsQueryConfig, Query, Filter } from 'src/plugins/data/public';
import { DatasourceDataPanelProps, DataType, StateSetter } from '../types';
import { ChildDragDropProvider, DragContextState } from '../drag_drop';
import { FieldItem } from './field_item';
@@ -74,6 +74,27 @@ const fieldTypeNames: Record = {
ip: i18n.translate('xpack.lens.datatypes.ipAddress', { defaultMessage: 'IP' }),
};
+// Wrapper around esQuery.buildEsQuery, handling errors (e.g. because a query can't be parsed) by
+// returning a query dsl object not matching anything
+function buildSafeEsQuery(
+ indexPattern: IIndexPattern,
+ query: Query,
+ filters: Filter[],
+ queryConfig: EsQueryConfig
+) {
+ try {
+ return esQuery.buildEsQuery(indexPattern, query, filters, queryConfig);
+ } catch (e) {
+ return {
+ bool: {
+ must_not: {
+ match_all: {},
+ },
+ },
+ };
+ }
+}
+
export function IndexPatternDataPanel({
setState,
state,
@@ -106,7 +127,7 @@ export function IndexPatternDataPanel({
timeFieldName: indexPatterns[id].timeFieldName,
}));
- const dslQuery = esQuery.buildEsQuery(
+ const dslQuery = buildSafeEsQuery(
indexPatterns[currentIndexPatternId] as IIndexPattern,
query,
filters,
diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
index 815725f4331a6..fabf9e9e9bfff 100644
--- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
+++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx
@@ -198,10 +198,12 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) {
className={`lnsFieldItem__info ${infoIsOpen ? 'lnsFieldItem__info-isOpen' : ''}`}
data-test-subj={`lnsFieldListPanelField-${field.name}`}
onClick={() => {
- togglePopover();
+ if (exists) {
+ togglePopover();
+ }
}}
onKeyPress={(event) => {
- if (event.key === 'ENTER') {
+ if (exists && event.key === 'ENTER') {
togglePopover();
}
}}
diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap
index a3bb32337f9f8..096f26eb22fe3 100644
--- a/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap
+++ b/x-pack/plugins/license_management/__jest__/__snapshots__/request_trial_extension.test.js.snap
@@ -1,9 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`RequestTrialExtension component should display when enterprise license is not active and trial has been used 1`] = `"Extend your trial If you’d like to continue using machine learning, advanced security, and our other awesome Platinum features , request an extension now.
"`;
+exports[`RequestTrialExtension component should display when enterprise license is not active and trial has been used 1`] = `"Extend your trial If you’d like to continue using machine learning, advanced security, and our other awesome subscription features , request an extension now.
"`;
-exports[`RequestTrialExtension component should display when license is active and trial has been used 1`] = `"Extend your trial If you’d like to continue using machine learning, advanced security, and our other awesome Platinum features , request an extension now.
"`;
+exports[`RequestTrialExtension component should display when license is active and trial has been used 1`] = `"Extend your trial If you’d like to continue using machine learning, advanced security, and our other awesome subscription features , request an extension now.
"`;
-exports[`RequestTrialExtension component should display when license is not active and trial has been used 1`] = `"Extend your trial If you’d like to continue using machine learning, advanced security, and our other awesome Platinum features , request an extension now.
"`;
+exports[`RequestTrialExtension component should display when license is not active and trial has been used 1`] = `"Extend your trial If you’d like to continue using machine learning, advanced security, and our other awesome subscription features , request an extension now.
"`;
-exports[`RequestTrialExtension component should display when platinum license is not active and trial has been used 1`] = `"Extend your trial If you’d like to continue using machine learning, advanced security, and our other awesome Platinum features , request an extension now.
"`;
+exports[`RequestTrialExtension component should display when platinum license is not active and trial has been used 1`] = `"Extend your trial If you’d like to continue using machine learning, advanced security, and our other awesome subscription features , request an extension now.
"`;
diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap
index cb2a41dadbe9e..0a5656aa266bc 100644
--- a/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap
+++ b/x-pack/plugins/license_management/__jest__/__snapshots__/revert_to_basic.test.js.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`RevertToBasic component should display when license is about to expire 1`] = `"Revert to Basic license You’ll revert to our free features and lose access to machine learning, advanced security, and other Platinum features .
"`;
+exports[`RevertToBasic component should display when license is about to expire 1`] = `"Revert to Basic license You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features .
"`;
-exports[`RevertToBasic component should display when license is expired 1`] = `"Revert to Basic license You’ll revert to our free features and lose access to machine learning, advanced security, and other Platinum features .
"`;
+exports[`RevertToBasic component should display when license is expired 1`] = `"Revert to Basic license You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features .
"`;
-exports[`RevertToBasic component should display when trial is active 1`] = `"Revert to Basic license You’ll revert to our free features and lose access to machine learning, advanced security, and other Platinum features .
"`;
+exports[`RevertToBasic component should display when trial is active 1`] = `"Revert to Basic license You’ll revert to our free features and lose access to machine learning, advanced security, and other subscription features .
"`;
diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap
index 9370b77e29560..9da8bb958941b 100644
--- a/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap
+++ b/x-pack/plugins/license_management/__jest__/__snapshots__/start_trial.test.js.snap
@@ -1,9 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`StartTrial component when trial is allowed display for basic license 1`] = `"Start a 30-day trial Experience what machine learning, advanced security, and all our other Platinum features have to offer.
"`;
+exports[`StartTrial component when trial is allowed display for basic license 1`] = `"Start a 30-day trial Experience what machine learning, advanced security, and all our other subscription features have to offer.
"`;
-exports[`StartTrial component when trial is allowed should display for expired enterprise license 1`] = `"Start a 30-day trial Experience what machine learning, advanced security, and all our other Platinum features have to offer.
"`;
+exports[`StartTrial component when trial is allowed should display for expired enterprise license 1`] = `"Start a 30-day trial Experience what machine learning, advanced security, and all our other subscription features have to offer.
"`;
-exports[`StartTrial component when trial is allowed should display for expired platinum license 1`] = `"Start a 30-day trial Experience what machine learning, advanced security, and all our other Platinum features have to offer.
"`;
+exports[`StartTrial component when trial is allowed should display for expired platinum license 1`] = `"Start a 30-day trial Experience what machine learning, advanced security, and all our other subscription features have to offer.
"`;
-exports[`StartTrial component when trial is allowed should display for gold license 1`] = `"Start a 30-day trial Experience what machine learning, advanced security, and all our other Platinum features have to offer.
"`;
+exports[`StartTrial component when trial is allowed should display for gold license 1`] = `"Start a 30-day trial Experience what machine learning, advanced security, and all our other subscription features have to offer.
"`;
diff --git a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap
index cc8cbfe679eff..f0feb826f956d 100644
--- a/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap
+++ b/x-pack/plugins/license_management/__jest__/__snapshots__/upload_license.test.tsx.snap
@@ -294,7 +294,7 @@ exports[`UploadLicense should display a modal when license requires acknowledgem
- Please address the errors in your form.
+ Please address the highlighted errors.
- Please address the errors in your form.
+ Please address the highlighted errors.
- Please address the errors in your form.
+ Please address the highlighted errors.
- Please address the errors in your form.
+ Please address the highlighted errors.
{
),
diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js b/x-pack/plugins/license_management/public/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js
index a1a46d8616554..24b51cccb4e45 100644
--- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js
+++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/revert_to_basic/revert_to_basic.js
@@ -82,13 +82,13 @@ export class RevertToBasic extends React.PureComponent {
),
diff --git a/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx b/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx
index 65d40f1de2009..7220f377cf386 100644
--- a/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx
+++ b/x-pack/plugins/license_management/public/application/sections/license_dashboard/start_trial/start_trial.tsx
@@ -94,14 +94,14 @@ export class StartTrial extends Component {
),
@@ -236,15 +236,15 @@ export class StartTrial extends Component {
const description = (
),
diff --git a/x-pack/plugins/licensing/kibana.json b/x-pack/plugins/licensing/kibana.json
index 9edaa726c6ba9..2d38a82271eb0 100644
--- a/x-pack/plugins/licensing/kibana.json
+++ b/x-pack/plugins/licensing/kibana.json
@@ -4,5 +4,6 @@
"kibanaVersion": "kibana",
"configPath": ["xpack", "licensing"],
"server": true,
- "ui": true
+ "ui": true,
+ "requiredBundles": ["kibanaReact"]
}
diff --git a/x-pack/plugins/lists/common/constants.mock.ts b/x-pack/plugins/lists/common/constants.mock.ts
index 185de02d555b7..7f7a90eeba5a2 100644
--- a/x-pack/plugins/lists/common/constants.mock.ts
+++ b/x-pack/plugins/lists/common/constants.mock.ts
@@ -41,6 +41,8 @@ export const OPERATOR = 'included';
export const ENTRY_VALUE = 'some host name';
export const MATCH = 'match';
export const MATCH_ANY = 'match_any';
+export const MAX_IMPORT_PAYLOAD_BYTES = 40000000;
+export const IMPORT_BUFFER_SIZE = 1000;
export const LIST = 'list';
export const EXISTS = 'exists';
export const NESTED = 'nested';
diff --git a/x-pack/plugins/lists/common/constants.ts b/x-pack/plugins/lists/common/constants.ts
index 6cb88b19483ce..af29b3aa53ded 100644
--- a/x-pack/plugins/lists/common/constants.ts
+++ b/x-pack/plugins/lists/common/constants.ts
@@ -10,6 +10,7 @@
export const LIST_URL = '/api/lists';
export const LIST_INDEX = `${LIST_URL}/index`;
export const LIST_ITEM_URL = `${LIST_URL}/items`;
+export const LIST_PRIVILEGES_URL = `${LIST_URL}/privileges`;
/**
* Exception list routes
diff --git a/x-pack/plugins/lists/server/config.mock.ts b/x-pack/plugins/lists/server/config.mock.ts
new file mode 100644
index 0000000000000..3cf5040c73675
--- /dev/null
+++ b/x-pack/plugins/lists/server/config.mock.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;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import {
+ IMPORT_BUFFER_SIZE,
+ LIST_INDEX,
+ LIST_ITEM_INDEX,
+ MAX_IMPORT_PAYLOAD_BYTES,
+} from '../common/constants.mock';
+
+import { ConfigType } from './config';
+
+export const getConfigMock = (): Partial => ({
+ listIndex: LIST_INDEX,
+ listItemIndex: LIST_ITEM_INDEX,
+});
+
+export const getConfigMockDecoded = (): ConfigType => ({
+ enabled: true,
+ importBufferSize: IMPORT_BUFFER_SIZE,
+ listIndex: LIST_INDEX,
+ listItemIndex: LIST_ITEM_INDEX,
+ maxImportPayloadBytes: MAX_IMPORT_PAYLOAD_BYTES,
+});
diff --git a/x-pack/plugins/lists/server/config.test.ts b/x-pack/plugins/lists/server/config.test.ts
new file mode 100644
index 0000000000000..60501322dcfa2
--- /dev/null
+++ b/x-pack/plugins/lists/server/config.test.ts
@@ -0,0 +1,64 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { ConfigSchema, ConfigType } from './config';
+import { getConfigMock, getConfigMockDecoded } from './config.mock';
+
+describe('config_schema', () => {
+ test('it works with expected basic mock data set and defaults', () => {
+ expect(ConfigSchema.validate(getConfigMock())).toEqual(getConfigMockDecoded());
+ });
+
+ test('it throws if given an invalid value', () => {
+ const mock: Partial & { madeUpValue: string } = {
+ madeUpValue: 'something',
+ ...getConfigMock(),
+ };
+ expect(() => ConfigSchema.validate(mock)).toThrow(
+ '[madeUpValue]: definition for this key is missing'
+ );
+ });
+
+ test('it throws if the "maxImportPayloadBytes" value is 0', () => {
+ const mock: ConfigType = {
+ ...getConfigMockDecoded(),
+ maxImportPayloadBytes: 0,
+ };
+ expect(() => ConfigSchema.validate(mock)).toThrow(
+ '[maxImportPayloadBytes]: Value must be equal to or greater than [1].'
+ );
+ });
+
+ test('it throws if the "maxImportPayloadBytes" value is less than 0', () => {
+ const mock: ConfigType = {
+ ...getConfigMockDecoded(),
+ maxImportPayloadBytes: -1,
+ };
+ expect(() => ConfigSchema.validate(mock)).toThrow(
+ '[maxImportPayloadBytes]: Value must be equal to or greater than [1].'
+ );
+ });
+
+ test('it throws if the "importBufferSize" value is 0', () => {
+ const mock: ConfigType = {
+ ...getConfigMockDecoded(),
+ importBufferSize: 0,
+ };
+ expect(() => ConfigSchema.validate(mock)).toThrow(
+ '[importBufferSize]: Value must be equal to or greater than [1].'
+ );
+ });
+
+ test('it throws if the "importBufferSize" value is less than 0', () => {
+ const mock: ConfigType = {
+ ...getConfigMockDecoded(),
+ importBufferSize: -1,
+ };
+ expect(() => ConfigSchema.validate(mock)).toThrow(
+ '[importBufferSize]: Value must be equal to or greater than [1].'
+ );
+ });
+});
diff --git a/x-pack/plugins/lists/server/config.ts b/x-pack/plugins/lists/server/config.ts
index f2fa7e8801033..0fcc68419f8fe 100644
--- a/x-pack/plugins/lists/server/config.ts
+++ b/x-pack/plugins/lists/server/config.ts
@@ -8,8 +8,10 @@ import { TypeOf, schema } from '@kbn/config-schema';
export const ConfigSchema = schema.object({
enabled: schema.boolean({ defaultValue: true }),
+ importBufferSize: schema.number({ defaultValue: 1000, min: 1 }),
listIndex: schema.string({ defaultValue: '.lists' }),
listItemIndex: schema.string({ defaultValue: '.items' }),
+ maxImportPayloadBytes: schema.number({ defaultValue: 40000000, min: 1 }),
});
export type ConfigType = TypeOf;
diff --git a/x-pack/plugins/lists/server/create_config.ts b/x-pack/plugins/lists/server/create_config.ts
index 7e2e639ce7a35..e46c71798eb9f 100644
--- a/x-pack/plugins/lists/server/create_config.ts
+++ b/x-pack/plugins/lists/server/create_config.ts
@@ -12,12 +12,6 @@ import { ConfigType } from './config';
export const createConfig$ = (
context: PluginInitializerContext
-): Observable<
- Readonly<{
- enabled: boolean;
- listIndex: string;
- listItemIndex: string;
- }>
-> => {
+): Observable> => {
return context.config.create().pipe(map((config) => config));
};
diff --git a/x-pack/plugins/lists/server/plugin.ts b/x-pack/plugins/lists/server/plugin.ts
index cdd674a19ceb6..118bb2f927a64 100644
--- a/x-pack/plugins/lists/server/plugin.ts
+++ b/x-pack/plugins/lists/server/plugin.ts
@@ -48,7 +48,7 @@ export class ListPlugin
core.http.registerRouteHandlerContext('lists', this.createRouteHandlerContext());
const router = core.http.createRouter();
- initRoutes(router);
+ initRoutes(router, config, plugins.security);
return {
getExceptionListClient: (savedObjectsClient, user): ExceptionListClient => {
diff --git a/x-pack/plugins/lists/server/routes/import_list_item_route.ts b/x-pack/plugins/lists/server/routes/import_list_item_route.ts
index d75199140ea8e..2e629d7516dd1 100644
--- a/x-pack/plugins/lists/server/routes/import_list_item_route.ts
+++ b/x-pack/plugins/lists/server/routes/import_list_item_route.ts
@@ -4,50 +4,40 @@
* you may not use this file except in compliance with the Elastic License.
*/
-import { Readable } from 'stream';
-
import { IRouter } from 'kibana/server';
+import { schema } from '@kbn/config-schema';
import { LIST_ITEM_URL } from '../../common/constants';
import { buildRouteValidation, buildSiemResponse, transformError } from '../siem_server_deps';
import { validate } from '../../common/siem_common_deps';
-import { importListItemQuerySchema, importListItemSchema, listSchema } from '../../common/schemas';
-
-import { getListClient } from '.';
+import { importListItemQuerySchema, listSchema } from '../../common/schemas';
+import { ConfigType } from '../config';
-export interface HapiReadableStream extends Readable {
- hapi: {
- filename: string;
- };
-}
+import { createStreamFromBuffer } from './utils/create_stream_from_buffer';
-/**
- * Special interface since we are streaming in a file through a reader
- */
-export interface ImportListItemHapiFileSchema {
- file: HapiReadableStream;
-}
+import { getListClient } from '.';
-export const importListItemRoute = (router: IRouter): void => {
+export const importListItemRoute = (router: IRouter, config: ConfigType): void => {
router.post(
{
options: {
body: {
- output: 'stream',
+ accepts: ['multipart/form-data'],
+ maxBytes: config.maxImportPayloadBytes,
+ parse: false,
},
tags: ['access:lists'],
},
path: `${LIST_ITEM_URL}/_import`,
validate: {
- body: buildRouteValidation(
- importListItemSchema
- ),
+ body: schema.buffer(),
query: buildRouteValidation(importListItemQuerySchema),
},
},
async (context, request, response) => {
const siemResponse = buildSiemResponse(response);
try {
+ const stream = createStreamFromBuffer(request.body);
const { deserializer, list_id: listId, serializer, type } = request.query;
const lists = getListClient(context);
if (listId != null) {
@@ -63,7 +53,7 @@ export const importListItemRoute = (router: IRouter): void => {
listId,
meta: undefined,
serializer: list.serializer,
- stream: request.body.file,
+ stream,
type: list.type,
});
@@ -74,26 +64,21 @@ export const importListItemRoute = (router: IRouter): void => {
return response.ok({ body: validated ?? {} });
}
} else if (type != null) {
- const { filename } = request.body.file.hapi;
- // TODO: Should we prevent the same file from being uploaded multiple times?
- const list = await lists.createListIfItDoesNotExist({
- description: `File uploaded from file system of ${filename}`,
+ const importedList = await lists.importListItemsToStream({
deserializer,
- id: filename,
+ listId: undefined,
meta: undefined,
- name: filename,
serializer,
+ stream,
type,
});
- await lists.importListItemsToStream({
- deserializer: list.deserializer,
- listId: list.id,
- meta: undefined,
- serializer: list.serializer,
- stream: request.body.file,
- type: list.type,
- });
- const [validated, errors] = validate(list, listSchema);
+ if (importedList == null) {
+ return siemResponse.error({
+ body: 'Unable to parse a valid fileName during import',
+ statusCode: 400,
+ });
+ }
+ const [validated, errors] = validate(importedList, listSchema);
if (errors != null) {
return siemResponse.error({ body: errors, statusCode: 500 });
} else {
diff --git a/x-pack/plugins/lists/server/routes/init_routes.ts b/x-pack/plugins/lists/server/routes/init_routes.ts
index e74fa471734b0..fef7f19f02df2 100644
--- a/x-pack/plugins/lists/server/routes/init_routes.ts
+++ b/x-pack/plugins/lists/server/routes/init_routes.ts
@@ -6,6 +6,11 @@
import { IRouter } from 'kibana/server';
+import { SecurityPluginSetup } from '../../../security/server';
+import { ConfigType } from '../config';
+
+import { readPrivilegesRoute } from './read_privileges_route';
+
import {
createExceptionListItemRoute,
createExceptionListRoute,
@@ -36,7 +41,11 @@ import {
updateListRoute,
} from '.';
-export const initRoutes = (router: IRouter): void => {
+export const initRoutes = (
+ router: IRouter,
+ config: ConfigType,
+ security: SecurityPluginSetup | null | undefined
+): void => {
// lists
createListRoute(router);
readListRoute(router);
@@ -44,6 +53,7 @@ export const initRoutes = (router: IRouter): void => {
deleteListRoute(router);
patchListRoute(router);
findListRoute(router);
+ readPrivilegesRoute(router, security);
// list items
createListItemRoute(router);
@@ -52,7 +62,7 @@ export const initRoutes = (router: IRouter): void => {
deleteListItemRoute(router);
patchListItemRoute(router);
exportListItemRoute(router);
- importListItemRoute(router);
+ importListItemRoute(router, config);
findListItemRoute(router);
// indexes of lists
diff --git a/x-pack/plugins/lists/server/routes/read_privileges_route.ts b/x-pack/plugins/lists/server/routes/read_privileges_route.ts
new file mode 100644
index 0000000000000..892b6406a28ec
--- /dev/null
+++ b/x-pack/plugins/lists/server/routes/read_privileges_route.ts
@@ -0,0 +1,60 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { IRouter } from 'kibana/server';
+import { merge } from 'lodash/fp';
+
+import { SecurityPluginSetup } from '../../../security/server';
+import { LIST_PRIVILEGES_URL } from '../../common/constants';
+import { buildSiemResponse, readPrivileges, transformError } from '../siem_server_deps';
+
+import { getListClient } from './utils';
+
+export const readPrivilegesRoute = (
+ router: IRouter,
+ security: SecurityPluginSetup | null | undefined
+): void => {
+ router.get(
+ {
+ options: {
+ tags: ['access:lists'],
+ },
+ path: LIST_PRIVILEGES_URL,
+ validate: false,
+ },
+ async (context, request, response) => {
+ const siemResponse = buildSiemResponse(response);
+ try {
+ const clusterClient = context.core.elasticsearch.legacy.client;
+ const lists = getListClient(context);
+ const clusterPrivilegesLists = await readPrivileges(
+ clusterClient.callAsCurrentUser,
+ lists.getListIndex()
+ );
+ const clusterPrivilegesListItems = await readPrivileges(
+ clusterClient.callAsCurrentUser,
+ lists.getListIndex()
+ );
+ const privileges = merge(
+ {
+ listItems: clusterPrivilegesListItems,
+ lists: clusterPrivilegesLists,
+ },
+ {
+ is_authenticated: security?.authc.isAuthenticated(request) ?? false,
+ }
+ );
+ return response.ok({ body: privileges });
+ } catch (err) {
+ const error = transformError(err);
+ return siemResponse.error({
+ body: error.message,
+ statusCode: error.statusCode,
+ });
+ }
+ }
+ );
+};
diff --git a/x-pack/plugins/lists/server/routes/utils/create_stream_from_buffer.ts b/x-pack/plugins/lists/server/routes/utils/create_stream_from_buffer.ts
new file mode 100644
index 0000000000000..3dcf03617bcbc
--- /dev/null
+++ b/x-pack/plugins/lists/server/routes/utils/create_stream_from_buffer.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import { Readable } from 'stream';
+
+export const createStreamFromBuffer = (buffer: Buffer): Readable => {
+ const stream = new Readable();
+ stream.push(buffer);
+ stream.push(null);
+ return stream;
+};
diff --git a/x-pack/plugins/lists/server/scripts/get_privileges.sh b/x-pack/plugins/lists/server/scripts/get_privileges.sh
new file mode 100755
index 0000000000000..4c02747f3c56c
--- /dev/null
+++ b/x-pack/plugins/lists/server/scripts/get_privileges.sh
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+#
+# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+# or more contributor license agreements. Licensed under the Elastic License;
+# you may not use this file except in compliance with the Elastic License.
+#
+
+set -e
+./check_env_variables.sh
+
+# Example: ./get_privileges.sh
+curl -s -k \
+ -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \
+ -X GET ${KIBANA_URL}${SPACE_URL}/api/lists/privileges | jq .
diff --git a/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts b/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts
index a283269271bd0..ad1511e28f80a 100644
--- a/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts
+++ b/x-pack/plugins/lists/server/services/items/buffer_lines.test.ts
@@ -4,15 +4,44 @@
* you may not use this file except in compliance with the Elastic License.
*/
+import { IMPORT_BUFFER_SIZE } from '../../../common/constants.mock';
+
import { BufferLines } from './buffer_lines';
import { TestReadable } from './test_readable.mock';
describe('buffer_lines', () => {
+ test('it will throw if given a buffer size of zero', () => {
+ expect(() => {
+ new BufferLines({ bufferSize: 0, input: new TestReadable() });
+ }).toThrow('bufferSize must be greater than zero');
+ });
+
+ test('it will throw if given a buffer size of -1', () => {
+ expect(() => {
+ new BufferLines({ bufferSize: -1, input: new TestReadable() });
+ }).toThrow('bufferSize must be greater than zero');
+ });
+
test('it can read a single line', (done) => {
const input = new TestReadable();
input.push('line one\n');
input.push(null);
- const bufferedLine = new BufferLines({ input });
+ const bufferedLine = new BufferLines({ bufferSize: IMPORT_BUFFER_SIZE, input });
+ let linesToTest: string[] = [];
+ bufferedLine.on('lines', (lines: string[]) => {
+ linesToTest = [...linesToTest, ...lines];
+ });
+ bufferedLine.on('close', () => {
+ expect(linesToTest).toEqual(['line one']);
+ done();
+ });
+ });
+
+ test('it can read a single line using a buffer size of 1', (done) => {
+ const input = new TestReadable();
+ input.push('line one\n');
+ input.push(null);
+ const bufferedLine = new BufferLines({ bufferSize: 1, input });
let linesToTest: string[] = [];
bufferedLine.on('lines', (lines: string[]) => {
linesToTest = [...linesToTest, ...lines];
@@ -28,7 +57,23 @@ describe('buffer_lines', () => {
input.push('line one\n');
input.push('line two\n');
input.push(null);
- const bufferedLine = new BufferLines({ input });
+ const bufferedLine = new BufferLines({ bufferSize: IMPORT_BUFFER_SIZE, input });
+ let linesToTest: string[] = [];
+ bufferedLine.on('lines', (lines: string[]) => {
+ linesToTest = [...linesToTest, ...lines];
+ });
+ bufferedLine.on('close', () => {
+ expect(linesToTest).toEqual(['line one', 'line two']);
+ done();
+ });
+ });
+
+ test('it can read two lines using a buffer size of 1', (done) => {
+ const input = new TestReadable();
+ input.push('line one\n');
+ input.push('line two\n');
+ input.push(null);
+ const bufferedLine = new BufferLines({ bufferSize: 1, input });
let linesToTest: string[] = [];
bufferedLine.on('lines', (lines: string[]) => {
linesToTest = [...linesToTest, ...lines];
@@ -44,7 +89,7 @@ describe('buffer_lines', () => {
input.push('line one\n');
input.push('line one\n');
input.push(null);
- const bufferedLine = new BufferLines({ input });
+ const bufferedLine = new BufferLines({ bufferSize: IMPORT_BUFFER_SIZE, input });
let linesToTest: string[] = [];
bufferedLine.on('lines', (lines: string[]) => {
linesToTest = [...linesToTest, ...lines];
@@ -58,7 +103,7 @@ describe('buffer_lines', () => {
test('it can close out without writing any lines', (done) => {
const input = new TestReadable();
input.push(null);
- const bufferedLine = new BufferLines({ input });
+ const bufferedLine = new BufferLines({ bufferSize: IMPORT_BUFFER_SIZE, input });
let linesToTest: string[] = [];
bufferedLine.on('lines', (lines: string[]) => {
linesToTest = [...linesToTest, ...lines];
@@ -71,7 +116,7 @@ describe('buffer_lines', () => {
test('it can read 200 lines', (done) => {
const input = new TestReadable();
- const bufferedLine = new BufferLines({ input });
+ const bufferedLine = new BufferLines({ bufferSize: IMPORT_BUFFER_SIZE, input });
let linesToTest: string[] = [];
const size200: string[] = new Array(200).fill(null).map((_, index) => `${index}\n`);
size200.forEach((element) => input.push(element));
@@ -84,4 +129,66 @@ describe('buffer_lines', () => {
done();
});
});
+
+ test('it can read an example multi-part message', (done) => {
+ const input = new TestReadable();
+ input.push('--boundary\n');
+ input.push('Content-type: text/plain\n');
+ input.push('Content-Disposition: form-data; name="fieldName"; filename="filename.text"\n');
+ input.push('\n');
+ input.push('127.0.0.1\n');
+ input.push('127.0.0.2\n');
+ input.push('127.0.0.3\n');
+ input.push('\n');
+ input.push('--boundary--\n');
+ input.push(null);
+ const bufferedLine = new BufferLines({ bufferSize: IMPORT_BUFFER_SIZE, input });
+ let linesToTest: string[] = [];
+ bufferedLine.on('lines', (lines: string[]) => {
+ linesToTest = [...linesToTest, ...lines];
+ });
+ bufferedLine.on('close', () => {
+ expect(linesToTest).toEqual(['127.0.0.1', '127.0.0.2', '127.0.0.3']);
+ done();
+ });
+ });
+
+ test('it can read an empty multi-part message', (done) => {
+ const input = new TestReadable();
+ input.push('--boundary\n');
+ input.push('Content-type: text/plain\n');
+ input.push('Content-Disposition: form-data; name="fieldName"; filename="filename.text"\n');
+ input.push('\n');
+ input.push('\n');
+ input.push('--boundary--\n');
+ input.push(null);
+ const bufferedLine = new BufferLines({ bufferSize: IMPORT_BUFFER_SIZE, input });
+ let linesToTest: string[] = [];
+ bufferedLine.on('lines', (lines: string[]) => {
+ linesToTest = [...linesToTest, ...lines];
+ });
+ bufferedLine.on('close', () => {
+ expect(linesToTest).toEqual([]);
+ done();
+ });
+ });
+
+ test('it can read a fileName from a multipart message', (done) => {
+ const input = new TestReadable();
+ input.push('--boundary\n');
+ input.push('Content-type: text/plain\n');
+ input.push('Content-Disposition: form-data; name="fieldName"; filename="filename.text"\n');
+ input.push('\n');
+ input.push('--boundary--\n');
+ input.push(null);
+ const bufferedLine = new BufferLines({ bufferSize: IMPORT_BUFFER_SIZE, input });
+ let fileNameToTest: string;
+ bufferedLine.on('fileName', (fileName: string) => {
+ fileNameToTest = fileName;
+ });
+ bufferedLine.on('close', () => {
+ expect(fileNameToTest).toEqual('filename.text');
+ done();
+ });
+ });
});
diff --git a/x-pack/plugins/lists/server/services/items/buffer_lines.ts b/x-pack/plugins/lists/server/services/items/buffer_lines.ts
index 4ff84268f5e0c..dc257eadb7438 100644
--- a/x-pack/plugins/lists/server/services/items/buffer_lines.ts
+++ b/x-pack/plugins/lists/server/services/items/buffer_lines.ts
@@ -7,18 +7,50 @@
import readLine from 'readline';
import { Readable } from 'stream';
-const BUFFER_SIZE = 100;
-
export class BufferLines extends Readable {
private set = new Set();
- constructor({ input }: { input: NodeJS.ReadableStream }) {
+ private boundary: string | null = null;
+ private readableText: boolean = false;
+ private paused: boolean = false;
+ private bufferSize: number;
+ constructor({ input, bufferSize }: { input: NodeJS.ReadableStream; bufferSize: number }) {
super({ encoding: 'utf-8' });
+ if (bufferSize <= 0) {
+ throw new RangeError('bufferSize must be greater than zero');
+ }
+ this.bufferSize = bufferSize;
+
const readline = readLine.createInterface({
input,
});
+ // We are parsing multipart/form-data involving boundaries as fast as we can to get
+ // * The filename if it exists and emit it
+ // * The actual content within the multipart/form-data
readline.on('line', (line) => {
- this.push(line);
+ if (this.boundary == null && line.startsWith('--')) {
+ this.boundary = `${line}--`;
+ } else if (this.boundary != null && !this.readableText && line.trim() !== '') {
+ if (line.startsWith('Content-Disposition')) {
+ const fileNameMatch = RegExp('filename="(?.+)"');
+ const matches = fileNameMatch.exec(line);
+ if (matches?.groups?.fileName != null) {
+ this.emit('fileName', matches.groups.fileName);
+ }
+ }
+ } else if (this.boundary != null && !this.readableText && line.trim() === '') {
+ // we are ready to be readable text now for parsing
+ this.readableText = true;
+ } else if (this.readableText && line.trim() === '') {
+ // skip and do nothing as this is either a empty line or an upcoming end is about to happen
+ } else if (this.boundary != null && this.readableText && line === this.boundary) {
+ // we are at the end of the stream
+ this.boundary = null;
+ this.readableText = false;
+ } else {
+ // we have actual content to push
+ this.push(line);
+ }
});
readline.on('close', () => {
@@ -26,23 +58,54 @@ export class BufferLines extends Readable {
});
}
- public _read(): void {
- // No operation but this is required to be implemented
+ public _read(): void {}
+
+ public pause(): this {
+ this.paused = true;
+ return this;
}
- public push(line: string | null): boolean {
- if (line == null) {
- this.emit('lines', Array.from(this.set));
- this.set.clear();
- this.emit('close');
- return true;
+ public resume(): this {
+ this.paused = false;
+ return this;
+ }
+
+ private emptyBuffer(): void {
+ const arrayFromSet = Array.from(this.set);
+ if (arrayFromSet.length === 0) {
+ this.emit('lines', []);
} else {
+ while (arrayFromSet.length) {
+ const spliced = arrayFromSet.splice(0, this.bufferSize);
+ this.emit('lines', spliced);
+ }
+ }
+ this.set.clear();
+ }
+
+ public push(line: string | null): boolean {
+ if (line != null) {
this.set.add(line);
- if (this.set.size > BUFFER_SIZE) {
- this.emit('lines', Array.from(this.set));
- this.set.clear();
+ if (this.paused) {
+ return false;
+ } else {
+ if (this.set.size > this.bufferSize) {
+ this.emptyBuffer();
+ }
return true;
+ }
+ } else {
+ if (this.paused) {
+ // If we paused but have buffered all of the available data
+ // we should do wait for 10(ms) and check again if we are paused
+ // or not.
+ setTimeout(() => {
+ this.push(line);
+ }, 10);
+ return false;
} else {
+ this.emptyBuffer();
+ this.emit('close');
return true;
}
}
diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.test.ts b/x-pack/plugins/lists/server/services/items/create_list_item.test.ts
index 7fbdc900fe2a4..76bd47d217107 100644
--- a/x-pack/plugins/lists/server/services/items/create_list_item.test.ts
+++ b/x-pack/plugins/lists/server/services/items/create_list_item.test.ts
@@ -36,6 +36,7 @@ describe('crete_list_item', () => {
body,
id: LIST_ITEM_ID,
index: LIST_ITEM_INDEX,
+ refresh: 'wait_for',
};
expect(options.callCluster).toBeCalledWith('index', expected);
});
diff --git a/x-pack/plugins/lists/server/services/items/create_list_item.ts b/x-pack/plugins/lists/server/services/items/create_list_item.ts
index 333f34946828a..aa17fc00b25c6 100644
--- a/x-pack/plugins/lists/server/services/items/create_list_item.ts
+++ b/x-pack/plugins/lists/server/services/items/create_list_item.ts
@@ -71,6 +71,7 @@ export const createListItem = async ({
body,
id,
index: listItemIndex,
+ refresh: 'wait_for',
});
return {
diff --git a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts
index 4ab1bfb856846..b2cc0da669e42 100644
--- a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts
+++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.test.ts
@@ -33,6 +33,7 @@ describe('crete_list_item_bulk', () => {
secondRecord,
],
index: LIST_ITEM_INDEX,
+ refresh: 'wait_for',
});
});
@@ -70,6 +71,7 @@ describe('crete_list_item_bulk', () => {
},
],
index: '.items',
+ refresh: 'wait_for',
});
});
});
diff --git a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts
index 447c0f6bf95cc..91e9587aa676a 100644
--- a/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts
+++ b/x-pack/plugins/lists/server/services/items/create_list_items_bulk.ts
@@ -80,9 +80,13 @@ export const createListItemsBulk = async ({
},
[]
);
-
- await callCluster('bulk', {
- body,
- index: listItemIndex,
- });
+ try {
+ await callCluster('bulk', {
+ body,
+ index: listItemIndex,
+ refresh: 'wait_for',
+ });
+ } catch (error) {
+ // TODO: Log out the error with return values from the bulk insert into another index or saved object
+ }
};
diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts b/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts
index ea338d9dd3791..b14bddb1268f8 100644
--- a/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts
+++ b/x-pack/plugins/lists/server/services/items/delete_list_item.test.ts
@@ -47,6 +47,7 @@ describe('delete_list_item', () => {
const deleteQuery = {
id: LIST_ITEM_ID,
index: LIST_ITEM_INDEX,
+ refresh: 'wait_for',
};
expect(options.callCluster).toBeCalledWith('delete', deleteQuery);
});
diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item.ts b/x-pack/plugins/lists/server/services/items/delete_list_item.ts
index b006aed6f6dde..baeced4b09995 100644
--- a/x-pack/plugins/lists/server/services/items/delete_list_item.ts
+++ b/x-pack/plugins/lists/server/services/items/delete_list_item.ts
@@ -28,6 +28,7 @@ export const deleteListItem = async ({
await callCluster('delete', {
id,
index: listItemIndex,
+ refresh: 'wait_for',
});
}
return listItem;
diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts
index bf1608334ef24..f658a51730d97 100644
--- a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts
+++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.test.ts
@@ -52,6 +52,7 @@ describe('delete_list_item_by_value', () => {
},
},
index: '.items',
+ refresh: 'wait_for',
};
expect(options.callCluster).toBeCalledWith('deleteByQuery', deleteByQuery);
});
diff --git a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts
index 3551cb75dc5bc..880402fca1bfa 100644
--- a/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts
+++ b/x-pack/plugins/lists/server/services/items/delete_list_item_by_value.ts
@@ -48,6 +48,7 @@ export const deleteListItemByValue = async ({
},
},
index: listItemIndex,
+ refresh: 'wait_for',
});
return listItems;
};
diff --git a/x-pack/plugins/lists/server/services/items/update_list_item.ts b/x-pack/plugins/lists/server/services/items/update_list_item.ts
index 24cd11cbb65e4..eb20f1cfe3b30 100644
--- a/x-pack/plugins/lists/server/services/items/update_list_item.ts
+++ b/x-pack/plugins/lists/server/services/items/update_list_item.ts
@@ -62,6 +62,7 @@ export const updateListItem = async ({
},
id: listItem.id,
index: listItemIndex,
+ refresh: 'wait_for',
});
return {
created_at: listItem.created_at,
diff --git a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.mock.ts b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.mock.ts
index b7e30e0a1c308..d868351fc4b33 100644
--- a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.mock.ts
+++ b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.mock.ts
@@ -5,14 +5,24 @@
*/
import { getCallClusterMock } from '../../../common/get_call_cluster.mock';
import { ImportListItemsToStreamOptions, WriteBufferToItemsOptions } from '../items';
-import { LIST_ID, LIST_ITEM_INDEX, META, TYPE, USER } from '../../../common/constants.mock';
+import {
+ LIST_ID,
+ LIST_INDEX,
+ LIST_ITEM_INDEX,
+ META,
+ TYPE,
+ USER,
+} from '../../../common/constants.mock';
+import { getConfigMockDecoded } from '../../config.mock';
import { TestReadable } from './test_readable.mock';
export const getImportListItemsToStreamOptionsMock = (): ImportListItemsToStreamOptions => ({
callCluster: getCallClusterMock(),
+ config: getConfigMockDecoded(),
deserializer: undefined,
listId: LIST_ID,
+ listIndex: LIST_INDEX,
listItemIndex: LIST_ITEM_INDEX,
meta: META,
serializer: undefined,
diff --git a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts
index 31b2b74c88431..2bffe338e9075 100644
--- a/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts
+++ b/x-pack/plugins/lists/server/services/items/write_lines_to_bulk_list_items.ts
@@ -8,20 +8,26 @@ import { Readable } from 'stream';
import { LegacyAPICaller } from 'kibana/server';
+import { createListIfItDoesNotExist } from '../lists/create_list_if_it_does_not_exist';
import {
DeserializerOrUndefined,
+ ListIdOrUndefined,
+ ListSchema,
MetaOrUndefined,
SerializerOrUndefined,
Type,
} from '../../../common/schemas';
+import { ConfigType } from '../../config';
import { BufferLines } from './buffer_lines';
import { createListItemsBulk } from './create_list_items_bulk';
export interface ImportListItemsToStreamOptions {
+ listId: ListIdOrUndefined;
+ config: ConfigType;
+ listIndex: string;
deserializer: DeserializerOrUndefined;
serializer: SerializerOrUndefined;
- listId: string;
stream: Readable;
callCluster: LegacyAPICaller;
listItemIndex: string;
@@ -31,34 +37,72 @@ export interface ImportListItemsToStreamOptions {
}
export const importListItemsToStream = ({
+ config,
deserializer,
serializer,
listId,
stream,
callCluster,
listItemIndex,
+ listIndex,
type,
user,
meta,
-}: ImportListItemsToStreamOptions): Promise => {
- return new Promise((resolve) => {
- const readBuffer = new BufferLines({ input: stream });
+}: ImportListItemsToStreamOptions): Promise => {
+ return new Promise((resolve) => {
+ const readBuffer = new BufferLines({ bufferSize: config.importBufferSize, input: stream });
+ let fileName: string | undefined;
+ let list: ListSchema | null = null;
+ readBuffer.on('fileName', async (fileNameEmitted: string) => {
+ readBuffer.pause();
+ fileName = fileNameEmitted;
+ if (listId == null) {
+ list = await createListIfItDoesNotExist({
+ callCluster,
+ description: `File uploaded from file system of ${fileNameEmitted}`,
+ deserializer,
+ id: fileNameEmitted,
+ listIndex,
+ meta,
+ name: fileNameEmitted,
+ serializer,
+ type,
+ user,
+ });
+ }
+ readBuffer.resume();
+ });
+
readBuffer.on('lines', async (lines: string[]) => {
- await writeBufferToItems({
- buffer: lines,
- callCluster,
- deserializer,
- listId,
- listItemIndex,
- meta,
- serializer,
- type,
- user,
- });
+ if (listId != null) {
+ await writeBufferToItems({
+ buffer: lines,
+ callCluster,
+ deserializer,
+ listId,
+ listItemIndex,
+ meta,
+ serializer,
+ type,
+ user,
+ });
+ } else if (fileName != null) {
+ await writeBufferToItems({
+ buffer: lines,
+ callCluster,
+ deserializer,
+ listId: fileName,
+ listItemIndex,
+ meta,
+ serializer,
+ type,
+ user,
+ });
+ }
});
readBuffer.on('close', () => {
- resolve();
+ resolve(list);
});
});
};
diff --git a/x-pack/plugins/lists/server/services/lists/create_list.test.ts b/x-pack/plugins/lists/server/services/lists/create_list.test.ts
index 43af08bcaf7ff..e328df710ebe1 100644
--- a/x-pack/plugins/lists/server/services/lists/create_list.test.ts
+++ b/x-pack/plugins/lists/server/services/lists/create_list.test.ts
@@ -52,6 +52,7 @@ describe('crete_list', () => {
body,
id: LIST_ID,
index: LIST_INDEX,
+ refresh: 'wait_for',
};
expect(options.callCluster).toBeCalledWith('index', expected);
});
diff --git a/x-pack/plugins/lists/server/services/lists/create_list.ts b/x-pack/plugins/lists/server/services/lists/create_list.ts
index 3925fa5f0170c..3d396cf4d5af9 100644
--- a/x-pack/plugins/lists/server/services/lists/create_list.ts
+++ b/x-pack/plugins/lists/server/services/lists/create_list.ts
@@ -67,6 +67,7 @@ export const createList = async ({
body,
id,
index: listIndex,
+ refresh: 'wait_for',
});
return {
id: response._id,
diff --git a/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts b/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts
new file mode 100644
index 0000000000000..84f5ac0308191
--- /dev/null
+++ b/x-pack/plugins/lists/server/services/lists/create_list_if_it_does_not_exist.ts
@@ -0,0 +1,71 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { LegacyAPICaller } from 'kibana/server';
+
+import {
+ Description,
+ DeserializerOrUndefined,
+ Id,
+ ListSchema,
+ MetaOrUndefined,
+ Name,
+ SerializerOrUndefined,
+ Type,
+} from '../../../common/schemas';
+
+import { getList } from './get_list';
+import { createList } from './create_list';
+
+export interface CreateListIfItDoesNotExistOptions {
+ id: Id;
+ type: Type;
+ name: Name;
+ deserializer: DeserializerOrUndefined;
+ serializer: SerializerOrUndefined;
+ description: Description;
+ callCluster: LegacyAPICaller;
+ listIndex: string;
+ user: string;
+ meta: MetaOrUndefined;
+ dateNow?: string;
+ tieBreaker?: string;
+}
+
+export const createListIfItDoesNotExist = async ({
+ id,
+ name,
+ type,
+ description,
+ deserializer,
+ callCluster,
+ listIndex,
+ user,
+ meta,
+ serializer,
+ dateNow,
+ tieBreaker,
+}: CreateListIfItDoesNotExistOptions): Promise => {
+ const list = await getList({ callCluster, id, listIndex });
+ if (list == null) {
+ return createList({
+ callCluster,
+ dateNow,
+ description,
+ deserializer,
+ id,
+ listIndex,
+ meta,
+ name,
+ serializer,
+ tieBreaker,
+ type,
+ user,
+ });
+ } else {
+ return list;
+ }
+};
diff --git a/x-pack/plugins/lists/server/services/lists/delete_list.test.ts b/x-pack/plugins/lists/server/services/lists/delete_list.test.ts
index b9f1ec4d400be..029b6226a7375 100644
--- a/x-pack/plugins/lists/server/services/lists/delete_list.test.ts
+++ b/x-pack/plugins/lists/server/services/lists/delete_list.test.ts
@@ -47,6 +47,7 @@ describe('delete_list', () => {
const deleteByQuery = {
body: { query: { term: { list_id: LIST_ID } } },
index: LIST_ITEM_INDEX,
+ refresh: 'wait_for',
};
expect(options.callCluster).toBeCalledWith('deleteByQuery', deleteByQuery);
});
@@ -59,6 +60,7 @@ describe('delete_list', () => {
const deleteQuery = {
id: LIST_ID,
index: LIST_INDEX,
+ refresh: 'wait_for',
};
expect(options.callCluster).toHaveBeenNthCalledWith(2, 'delete', deleteQuery);
});
diff --git a/x-pack/plugins/lists/server/services/lists/delete_list.ts b/x-pack/plugins/lists/server/services/lists/delete_list.ts
index 64359b7273274..152048ca9cac6 100644
--- a/x-pack/plugins/lists/server/services/lists/delete_list.ts
+++ b/x-pack/plugins/lists/server/services/lists/delete_list.ts
@@ -36,11 +36,13 @@ export const deleteList = async ({
},
},
index: listItemIndex,
+ refresh: 'wait_for',
});
await callCluster('delete', {
id,
index: listIndex,
+ refresh: 'wait_for',
});
return list;
}
diff --git a/x-pack/plugins/lists/server/services/lists/list_client.mock.ts b/x-pack/plugins/lists/server/services/lists/list_client.mock.ts
index 43a01a3ca62dc..e5036d561ddc6 100644
--- a/x-pack/plugins/lists/server/services/lists/list_client.mock.ts
+++ b/x-pack/plugins/lists/server/services/lists/list_client.mock.ts
@@ -9,7 +9,12 @@ import { getFoundListSchemaMock } from '../../../common/schemas/response/found_l
import { getListItemResponseMock } from '../../../common/schemas/response/list_item_schema.mock';
import { getListResponseMock } from '../../../common/schemas/response/list_schema.mock';
import { getCallClusterMock } from '../../../common/get_call_cluster.mock';
-import { LIST_INDEX, LIST_ITEM_INDEX } from '../../../common/constants.mock';
+import {
+ IMPORT_BUFFER_SIZE,
+ LIST_INDEX,
+ LIST_ITEM_INDEX,
+ MAX_IMPORT_PAYLOAD_BYTES,
+} from '../../../common/constants.mock';
import { ListClient } from './list_client';
@@ -59,8 +64,10 @@ export const getListClientMock = (): ListClient => {
callCluster: getCallClusterMock(),
config: {
enabled: true,
+ importBufferSize: IMPORT_BUFFER_SIZE,
listIndex: LIST_INDEX,
listItemIndex: LIST_ITEM_INDEX,
+ maxImportPayloadBytes: MAX_IMPORT_PAYLOAD_BYTES,
},
spaceId: 'default',
user: 'elastic',
diff --git a/x-pack/plugins/lists/server/services/lists/list_client.ts b/x-pack/plugins/lists/server/services/lists/list_client.ts
index be9da1a1c69f5..4acc2e7092491 100644
--- a/x-pack/plugins/lists/server/services/lists/list_client.ts
+++ b/x-pack/plugins/lists/server/services/lists/list_client.ts
@@ -70,6 +70,7 @@ import {
UpdateListItemOptions,
UpdateListOptions,
} from './list_client_types';
+import { createListIfItDoesNotExist } from './create_list_if_it_does_not_exist';
export class ListClient {
private readonly spaceId: string;
@@ -140,12 +141,20 @@ export class ListClient {
type,
meta,
}: CreateListIfItDoesNotExistOptions): Promise => {
- const list = await this.getList({ id });
- if (list == null) {
- return this.createList({ description, deserializer, id, meta, name, serializer, type });
- } else {
- return list;
- }
+ const { callCluster, user } = this;
+ const listIndex = this.getListIndex();
+ return createListIfItDoesNotExist({
+ callCluster,
+ description,
+ deserializer,
+ id,
+ listIndex,
+ meta,
+ name,
+ serializer,
+ type,
+ user,
+ });
};
public getListIndexExists = async (): Promise => {
@@ -325,13 +334,16 @@ export class ListClient {
listId,
stream,
meta,
- }: ImportListItemsToStreamOptions): Promise => {
- const { callCluster, user } = this;
+ }: ImportListItemsToStreamOptions): Promise => {
+ const { callCluster, user, config } = this;
const listItemIndex = this.getListItemIndex();
+ const listIndex = this.getListIndex();
return importListItemsToStream({
callCluster,
+ config,
deserializer,
listId,
+ listIndex,
listItemIndex,
meta,
serializer,
diff --git a/x-pack/plugins/lists/server/services/lists/list_client_types.ts b/x-pack/plugins/lists/server/services/lists/list_client_types.ts
index 26e147a6fa130..68a018fa2fc16 100644
--- a/x-pack/plugins/lists/server/services/lists/list_client_types.ts
+++ b/x-pack/plugins/lists/server/services/lists/list_client_types.ts
@@ -16,6 +16,7 @@ import {
Id,
IdOrUndefined,
ListId,
+ ListIdOrUndefined,
MetaOrUndefined,
Name,
NameOrUndefined,
@@ -86,9 +87,9 @@ export interface ExportListItemsToStreamOptions {
}
export interface ImportListItemsToStreamOptions {
+ listId: ListIdOrUndefined;
deserializer: DeserializerOrUndefined;
serializer: SerializerOrUndefined;
- listId: string;
type: Type;
stream: Readable;
meta: MetaOrUndefined;
diff --git a/x-pack/plugins/lists/server/services/lists/update_list.ts b/x-pack/plugins/lists/server/services/lists/update_list.ts
index c7cc30aaae908..f84ca787eaa7c 100644
--- a/x-pack/plugins/lists/server/services/lists/update_list.ts
+++ b/x-pack/plugins/lists/server/services/lists/update_list.ts
@@ -55,6 +55,7 @@ export const updateList = async ({
body: { doc },
id,
index: listIndex,
+ refresh: 'wait_for',
});
return {
created_at: list.created_at,
diff --git a/x-pack/plugins/lists/server/siem_server_deps.ts b/x-pack/plugins/lists/server/siem_server_deps.ts
index 87a623c7a1892..324103b7fb50d 100644
--- a/x-pack/plugins/lists/server/siem_server_deps.ts
+++ b/x-pack/plugins/lists/server/siem_server_deps.ts
@@ -17,4 +17,5 @@ export {
createBootstrapIndex,
getIndexExists,
buildRouteValidation,
+ readPrivileges,
} from '../../security_solution/server';
diff --git a/x-pack/plugins/logstash/kibana.json b/x-pack/plugins/logstash/kibana.json
index 1eb325dcc1610..5949d5db041f2 100644
--- a/x-pack/plugins/logstash/kibana.json
+++ b/x-pack/plugins/logstash/kibana.json
@@ -13,5 +13,6 @@
"security"
],
"server": true,
- "ui": true
+ "ui": true,
+ "requiredBundles": ["home"]
}
diff --git a/x-pack/plugins/logstash/public/application/components/upgrade_failure/__snapshots__/upgrade_failure.test.js.snap b/x-pack/plugins/logstash/public/application/components/upgrade_failure/__snapshots__/upgrade_failure.test.js.snap
index b37131d80168e..5f54513c427dd 100644
--- a/x-pack/plugins/logstash/public/application/components/upgrade_failure/__snapshots__/upgrade_failure.test.js.snap
+++ b/x-pack/plugins/logstash/public/application/components/upgrade_failure/__snapshots__/upgrade_failure.test.js.snap
@@ -159,7 +159,9 @@ exports[`UpgradeFailure component passes expected text for new pipeline 1`] = `
-
+
-
+
-
+
{
if (isKeyboardEvent(e)) {
- if (e.keyCode === keyCodes.ENTER) {
+ if (e.key === keys.ENTER) {
e.preventDefault();
this._togglePopover();
- } else if (e.keyCode === keyCodes.DOWN) {
+ } else if (e.key === keys.ARROW_DOWN) {
this._openPopover();
}
}
diff --git a/x-pack/plugins/ml/common/constants/field_types.ts b/x-pack/plugins/ml/common/constants/field_types.ts
index 9402e4c20e46f..93641fd45c499 100644
--- a/x-pack/plugins/ml/common/constants/field_types.ts
+++ b/x-pack/plugins/ml/common/constants/field_types.ts
@@ -17,3 +17,6 @@ export enum ML_JOB_FIELD_TYPES {
export const MLCATEGORY = 'mlcategory';
export const DOC_COUNT = 'doc_count';
+
+// List of system fields we don't want to display.
+export const OMIT_FIELDS: string[] = ['_source', '_type', '_index', '_id', '_version', '_score'];
diff --git a/x-pack/plugins/ml/common/constants/new_job.ts b/x-pack/plugins/ml/common/constants/new_job.ts
index 751413bb6485a..d5c532234fd2b 100644
--- a/x-pack/plugins/ml/common/constants/new_job.ts
+++ b/x-pack/plugins/ml/common/constants/new_job.ts
@@ -17,6 +17,7 @@ export enum CREATED_BY_LABEL {
MULTI_METRIC = 'multi-metric-wizard',
POPULATION = 'population-wizard',
CATEGORIZATION = 'categorization-wizard',
+ APM_TRANSACTION = 'ml-module-apm-transaction',
}
export const DEFAULT_MODEL_MEMORY_LIMIT = '10MB';
diff --git a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts
index e2c4f1bae1a10..744f9c4d759dd 100644
--- a/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts
+++ b/x-pack/plugins/ml/common/types/anomaly_detection_jobs/job.ts
@@ -65,7 +65,7 @@ export interface Detector {
function: string;
over_field_name?: string;
partition_field_name?: string;
- use_null?: string;
+ use_null?: boolean;
custom_rules?: CustomRule[];
}
export interface AnalysisLimits {
@@ -80,7 +80,7 @@ export interface DataDescription {
}
export interface ModelPlotConfig {
- enabled: boolean;
+ enabled?: boolean;
annotations_enabled?: boolean;
terms?: string;
}
diff --git a/x-pack/plugins/ml/kibana.json b/x-pack/plugins/ml/kibana.json
index f93e7bc19f960..a08b9b6d97116 100644
--- a/x-pack/plugins/ml/kibana.json
+++ b/x-pack/plugins/ml/kibana.json
@@ -25,5 +25,13 @@
"licenseManagement"
],
"server": true,
- "ui": true
+ "ui": true,
+ "requiredBundles": [
+ "esUiShared",
+ "kibanaUtils",
+ "kibanaReact",
+ "management",
+ "dashboard",
+ "savedObjects"
+ ]
}
diff --git a/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.test.tsx b/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.test.tsx
index 83e7b82986cf8..d71a180cd2206 100644
--- a/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.test.tsx
+++ b/x-pack/plugins/ml/public/application/components/controls/select_interval/select_interval.test.tsx
@@ -11,13 +11,17 @@ import { mount } from 'enzyme';
import { EuiSelect } from '@elastic/eui';
+import { UrlStateProvider } from '../../../util/url_state';
+
import { SelectInterval } from './select_interval';
describe('SelectInterval', () => {
test('creates correct initial selected value', () => {
const wrapper = mount(
-
+
+
+
);
const select = wrapper.find(EuiSelect);
@@ -29,7 +33,9 @@ describe('SelectInterval', () => {
test('currently selected value is updated correctly on click', (done) => {
const wrapper = mount(
-
+
+
+
);
const select = wrapper.find(EuiSelect).first();
diff --git a/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.test.tsx b/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.test.tsx
index 484a0c395f3f8..cb4f80bfe6809 100644
--- a/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.test.tsx
+++ b/x-pack/plugins/ml/public/application/components/controls/select_severity/select_severity.test.tsx
@@ -11,13 +11,17 @@ import { mount } from 'enzyme';
import { EuiSuperSelect } from '@elastic/eui';
+import { UrlStateProvider } from '../../../util/url_state';
+
import { SelectSeverity } from './select_severity';
describe('SelectSeverity', () => {
test('creates correct severity options and initial selected value', () => {
const wrapper = mount(
-
+
+
+
);
const select = wrapper.find(EuiSuperSelect);
@@ -65,7 +69,9 @@ describe('SelectSeverity', () => {
test('state for currently selected value is updated correctly on click', (done) => {
const wrapper = mount(
-
+
+
+
);
diff --git a/x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.js b/x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.js
index 056fd04857cba..1b33d68042295 100644
--- a/x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.js
+++ b/x-pack/plugins/ml/public/application/components/field_title_bar/field_title_bar.test.js
@@ -62,7 +62,7 @@ describe('FieldTitleBar', () => {
expect(hasClassName).toBeTruthy();
});
- test(`tooltip hovering`, () => {
+ test(`tooltip hovering`, (done) => {
const props = { card: { fieldName: 'foo', type: 'bar' } };
const wrapper = mountWithIntl( );
const container = wrapper.find({ className: 'field-name' });
@@ -70,9 +70,14 @@ describe('FieldTitleBar', () => {
expect(wrapper.find('EuiToolTip').children()).toHaveLength(1);
container.simulate('mouseover');
- expect(wrapper.find('EuiToolTip').children()).toHaveLength(2);
-
- container.simulate('mouseout');
- expect(wrapper.find('EuiToolTip').children()).toHaveLength(1);
+ // EuiToolTip mounts children after a 250ms delay
+ setTimeout(() => {
+ wrapper.update();
+ expect(wrapper.find('EuiToolTip').children()).toHaveLength(2);
+ container.simulate('mouseout');
+ wrapper.update();
+ expect(wrapper.find('EuiToolTip').children()).toHaveLength(1);
+ done();
+ }, 250);
});
});
diff --git a/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.js b/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.js
index f616f7cb1b866..7e37dc10ade33 100644
--- a/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.js
+++ b/x-pack/plugins/ml/public/application/components/field_type_icon/field_type_icon.test.js
@@ -35,7 +35,8 @@ describe('FieldTypeIcon', () => {
expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(1);
container.simulate('mouseover');
- expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(2);
+ // EuiToolTip mounts children after a 250ms delay
+ setTimeout(() => expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(2), 250);
container.simulate('mouseout');
expect(typeIconComponent.find('EuiToolTip').children()).toHaveLength(1);
diff --git a/x-pack/plugins/ml/public/application/components/ml_in_memory_table/types.ts b/x-pack/plugins/ml/public/application/components/ml_in_memory_table/types.ts
index b85fb634891e5..05b941f2544b4 100644
--- a/x-pack/plugins/ml/public/application/components/ml_in_memory_table/types.ts
+++ b/x-pack/plugins/ml/public/application/components/ml_in_memory_table/types.ts
@@ -48,7 +48,7 @@ type BUTTON_ICON_COLORS = any;
type ButtonIconColorsFunc = (item: T) => BUTTON_ICON_COLORS; // (item) => oneOf(ICON_BUTTON_COLORS)
interface DefaultItemActionType {
type?: 'icon' | 'button';
- name: string;
+ name: ReactNode;
description: string;
onClick?(item: T): void;
href?: string;
diff --git a/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx b/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx
index 859d649416267..3a4875fa243fd 100644
--- a/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx
+++ b/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx
@@ -60,7 +60,7 @@ function getTabs(disableLinks: boolean): Tab[] {
name: i18n.translate('xpack.ml.navMenu.settingsTabLinkText', {
defaultMessage: 'Settings',
}),
- disabled: false,
+ disabled: disableLinks,
},
];
}
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
index aa637f71db1cc..618ea5184007d 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts
@@ -121,16 +121,24 @@ export interface DfAnalyticsExplainResponse {
}
export interface Eval {
- meanSquaredError: number | string;
+ mse: number | string;
+ msle: number | string;
+ huber: number | string;
rSquared: number | string;
error: null | string;
}
export interface RegressionEvaluateResponse {
regression: {
+ huber: {
+ value: number;
+ };
mse: {
value: number;
};
+ msle: {
+ value: number;
+ };
r_squared: {
value: number;
};
@@ -414,19 +422,37 @@ export const useRefreshAnalyticsList = (
const DEFAULT_SIG_FIGS = 3;
-export function getValuesFromResponse(response: RegressionEvaluateResponse) {
- let meanSquaredError = response?.regression?.mse?.value;
+interface RegressionEvaluateExtractedResponse {
+ mse: number | string;
+ msle: number | string;
+ huber: number | string;
+ r_squared: number | string;
+}
- if (meanSquaredError) {
- meanSquaredError = Number(meanSquaredError.toPrecision(DEFAULT_SIG_FIGS));
- }
+export const EMPTY_STAT = '--';
- let rSquared = response?.regression?.r_squared?.value;
- if (rSquared) {
- rSquared = Number(rSquared.toPrecision(DEFAULT_SIG_FIGS));
+export function getValuesFromResponse(response: RegressionEvaluateResponse) {
+ const results: RegressionEvaluateExtractedResponse = {
+ mse: EMPTY_STAT,
+ msle: EMPTY_STAT,
+ huber: EMPTY_STAT,
+ r_squared: EMPTY_STAT,
+ };
+
+ if (response?.regression) {
+ for (const statType in response.regression) {
+ if (response.regression.hasOwnProperty(statType)) {
+ let currentStatValue =
+ response.regression[statType as keyof RegressionEvaluateResponse['regression']]?.value;
+ if (currentStatValue) {
+ currentStatValue = Number(currentStatValue.toPrecision(DEFAULT_SIG_FIGS));
+ }
+ results[statType as keyof RegressionEvaluateExtractedResponse] = currentStatValue;
+ }
+ }
}
- return { meanSquaredError, rSquared };
+ return results;
}
interface ResultsSearchBoolQuery {
bool: Dictionary;
@@ -490,13 +516,22 @@ export function getEvalQueryBody({
return query;
}
+export enum REGRESSION_STATS {
+ MSE = 'mse',
+ MSLE = 'msle',
+ R_SQUARED = 'rSquared',
+ HUBER = 'huber',
+}
+
interface EvaluateMetrics {
classification: {
multiclass_confusion_matrix: object;
};
regression: {
r_squared: object;
- mean_squared_error: object;
+ mse: object;
+ msle: object;
+ huber: object;
};
}
@@ -541,7 +576,9 @@ export const loadEvalData = async ({
},
regression: {
r_squared: {},
- mean_squared_error: {},
+ mse: {},
+ msle: {},
+ huber: {},
},
};
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx
index def6acdae14e3..ff8797bc523c1 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/analysis_fields_table.tsx
@@ -85,16 +85,27 @@ export const AnalysisFieldsTable: FC<{
includes: string[];
loadingItems: boolean;
setFormState: React.Dispatch>;
+ minimumFieldsRequiredMessage?: string;
+ setMinimumFieldsRequiredMessage: React.Dispatch>;
tableItems: FieldSelectionItem[];
-}> = ({ dependentVariable, includes, loadingItems, setFormState, tableItems }) => {
+ unsupportedFieldsError?: string;
+ setUnsupportedFieldsError: React.Dispatch>;
+}> = ({
+ dependentVariable,
+ includes,
+ loadingItems,
+ setFormState,
+ minimumFieldsRequiredMessage,
+ setMinimumFieldsRequiredMessage,
+ tableItems,
+ unsupportedFieldsError,
+ setUnsupportedFieldsError,
+}) => {
const [sortableProperties, setSortableProperties] = useState();
const [currentPaginationData, setCurrentPaginationData] = useState<{
pageIndex: number;
itemsPerPage: number;
}>({ pageIndex: 0, itemsPerPage: 5 });
- const [minimumFieldsRequiredMessage, setMinimumFieldsRequiredMessage] = useState<
- undefined | string
- >(undefined);
useEffect(() => {
if (includes.length === 0 && tableItems.length > 0) {
@@ -164,8 +175,21 @@ export const AnalysisFieldsTable: FC<{
label={i18n.translate('xpack.ml.dataframe.analytics.create.includedFieldsLabel', {
defaultMessage: 'Included fields',
})}
- isInvalid={minimumFieldsRequiredMessage !== undefined}
- error={minimumFieldsRequiredMessage}
+ fullWidth
+ isInvalid={
+ minimumFieldsRequiredMessage !== undefined || unsupportedFieldsError !== undefined
+ }
+ error={[
+ ...(minimumFieldsRequiredMessage !== undefined ? [minimumFieldsRequiredMessage] : []),
+ ...(unsupportedFieldsError !== undefined
+ ? [
+ i18n.translate('xpack.ml.dataframe.analytics.create.unsupportedFieldsError', {
+ defaultMessage: 'Invalid. {message}',
+ values: { message: unsupportedFieldsError },
+ }),
+ ]
+ : []),
+ ]}
>
@@ -209,9 +233,10 @@ export const AnalysisFieldsTable: FC<{
) {
selection = [dependentVariable];
}
- // If nothing selected show minimum fields required message and don't update form yet
+ // If includes is empty show minimum fields required message and don't update form yet
if (selection.length === 0) {
setMinimumFieldsRequiredMessage(minimumFieldsMessage);
+ setUnsupportedFieldsError(undefined);
} else {
setMinimumFieldsRequiredMessage(undefined);
setFormState({ includes: selection });
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx
index b83dd2e4329e0..571c7731822c0 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx
@@ -71,12 +71,11 @@ export const ConfigurationStepForm: FC = ({
EuiComboBoxOptionOption[]
>([]);
const [includesTableItems, setIncludesTableItems] = useState([]);
- const [maxDistinctValuesError, setMaxDistinctValuesError] = useState(
- undefined
- );
- const [unsupportedFieldsError, setUnsupportedFieldsError] = useState(
- undefined
- );
+ const [maxDistinctValuesError, setMaxDistinctValuesError] = useState();
+ const [unsupportedFieldsError, setUnsupportedFieldsError] = useState();
+ const [minimumFieldsRequiredMessage, setMinimumFieldsRequiredMessage] = useState<
+ undefined | string
+ >();
const { setEstimatedModelMemoryLimit, setFormState } = actions;
const { estimatedModelMemoryLimit, form, isJobCreated, requestMessages } = state;
@@ -121,6 +120,7 @@ export const ConfigurationStepForm: FC = ({
dependentVariableEmpty ||
jobType === undefined ||
maxDistinctValuesError !== undefined ||
+ minimumFieldsRequiredMessage !== undefined ||
requiredFieldsError !== undefined ||
unsupportedFieldsError !== undefined;
@@ -404,32 +404,22 @@ export const ConfigurationStepForm: FC = ({
)}
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/form_options_validation.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/form_options_validation.ts
index bf3ab01549139..0935ed15a1a4a 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/form_options_validation.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/form_options_validation.ts
@@ -12,9 +12,6 @@ import { BASIC_NUMERICAL_TYPES, EXTENDED_NUMERICAL_TYPES } from '../../../../com
export const CATEGORICAL_TYPES = new Set(['ip', 'keyword']);
-// List of system fields we want to ignore for the numeric field check.
-export const OMIT_FIELDS: string[] = ['_source', '_type', '_index', '_id', '_version', '_score'];
-
// Regression supports numeric fields. Classification supports categorical, numeric, and boolean.
export const shouldAddAsDepVarOption = (field: Field, jobType: AnalyticsJobType) => {
if (field.id === EVENT_RATE_FIELD_ID) return false;
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx
index 0a4ba67831818..88c89df86b29a 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/supported_fields_message.tsx
@@ -11,8 +11,9 @@ import { FormattedMessage } from '@kbn/i18n/react';
import { AnalyticsJobType } from '../../../analytics_management/hooks/use_create_analytics_form/state';
import { ANALYSIS_CONFIG_TYPE } from '../../../../common/analytics';
import { Field, EVENT_RATE_FIELD_ID } from '../../../../../../../common/types/fields';
+import { OMIT_FIELDS } from '../../../../../../../common/constants/field_types';
import { BASIC_NUMERICAL_TYPES, EXTENDED_NUMERICAL_TYPES } from '../../../../common/fields';
-import { OMIT_FIELDS, CATEGORICAL_TYPES } from './form_options_validation';
+import { CATEGORICAL_TYPES } from './form_options_validation';
import { ES_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public';
import { newJobCapsService } from '../../../../../services/new_job_capabilities_service';
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/progress_stats.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/progress_stats.tsx
index a50254334526c..c87f0f4206feb 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/progress_stats.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/create_step/progress_stats.tsx
@@ -15,13 +15,15 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useMlKibana } from '../../../../../contexts/kibana';
-import { getDataFrameAnalyticsProgressPhase } from '../../../analytics_management/components/analytics_list/common';
+import {
+ getDataFrameAnalyticsProgressPhase,
+ DATA_FRAME_TASK_STATE,
+} from '../../../analytics_management/components/analytics_list/common';
import { isGetDataFrameAnalyticsStatsResponseOk } from '../../../analytics_management/services/analytics_service/get_analytics';
import { ml } from '../../../../../services/ml_api_service';
import { DataFrameAnalyticsId } from '../../../../common/analytics';
export const PROGRESS_REFRESH_INTERVAL_MS = 1000;
-const FAILED = 'failed';
export const ProgressStats: FC<{ jobId: DataFrameAnalyticsId }> = ({ jobId }) => {
const [initialized, setInitialized] = useState(false);
@@ -54,7 +56,7 @@ export const ProgressStats: FC<{ jobId: DataFrameAnalyticsId }> = ({ jobId }) =>
if (jobStats !== undefined) {
const progressStats = getDataFrameAnalyticsProgressPhase(jobStats);
- if (jobStats.state === FAILED) {
+ if (jobStats.state === DATA_FRAME_TASK_STATE.FAILED) {
clearInterval(interval);
setFailedJobMessage(
jobStats.failure_reason ||
@@ -70,8 +72,9 @@ export const ProgressStats: FC<{ jobId: DataFrameAnalyticsId }> = ({ jobId }) =>
setCurrentProgress(progressStats);
if (
- progressStats.currentPhase === progressStats.totalPhases &&
- progressStats.progress === 100
+ (progressStats.currentPhase === progressStats.totalPhases &&
+ progressStats.progress === 100) ||
+ jobStats.state === DATA_FRAME_TASK_STATE.STOPPED
) {
clearInterval(interval);
}
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx
index a4d86b48006e8..8a41eb4b8a865 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_details.tsx
@@ -26,7 +26,7 @@ export const DetailsStepDetails: FC<{ setCurrentStep: any; state: State }> = ({
state,
}) => {
const { form, isJobCreated } = state;
- const { description, jobId, destinationIndex } = form;
+ const { description, jobId, destinationIndex, resultsField } = form;
const detailsFirstCol: ListItems[] = [
{
@@ -37,6 +37,19 @@ export const DetailsStepDetails: FC<{ setCurrentStep: any; state: State }> = ({
},
];
+ if (
+ resultsField !== undefined &&
+ typeof resultsField === 'string' &&
+ resultsField.trim() !== ''
+ ) {
+ detailsFirstCol.push({
+ title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.resultsField', {
+ defaultMessage: 'Results field',
+ }),
+ description: resultsField,
+ });
+ }
+
const detailsSecondCol: ListItems[] = [
{
title: i18n.translate('xpack.ml.dataframe.analytics.create.configDetails.jobDescription', {
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx
index d846ae95c2c7e..168d5e31f57c3 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/details_step/details_step_form.tsx
@@ -47,6 +47,7 @@ export const DetailsStepForm: FC = ({
jobIdExists,
jobIdInvalidMaxLength,
jobIdValid,
+ resultsField,
} = form;
const forceInput = useRef(null);
@@ -195,6 +196,22 @@ export const DetailsStepForm: FC = ({
data-test-subj="mlAnalyticsCreateJobFlyoutDestinationIndexInput"
/>
+
+ setFormState({ resultsField: e.target.value })}
+ data-test-subj="mlAnalyticsCreateJobWizardResultsFieldInput"
+ />
+
= ({ jobConfig, jobStatus, searchQuery }) => {
const {
@@ -82,18 +94,19 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery })
genErrorEval.eval &&
isRegressionEvaluateResponse(genErrorEval.eval)
) {
- const { meanSquaredError, rSquared } = getValuesFromResponse(genErrorEval.eval);
+ const { mse, msle, huber, r_squared } = getValuesFromResponse(genErrorEval.eval);
setGeneralizationEval({
- meanSquaredError,
- rSquared,
+ mse,
+ msle,
+ huber,
+ rSquared: r_squared,
error: null,
});
setIsLoadingGeneralization(false);
} else {
setIsLoadingGeneralization(false);
setGeneralizationEval({
- meanSquaredError: '--',
- rSquared: '--',
+ ...EMPTY_STATS,
error: genErrorEval.error,
});
}
@@ -118,18 +131,19 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery })
trainingErrorEval.eval &&
isRegressionEvaluateResponse(trainingErrorEval.eval)
) {
- const { meanSquaredError, rSquared } = getValuesFromResponse(trainingErrorEval.eval);
+ const { mse, msle, huber, r_squared } = getValuesFromResponse(trainingErrorEval.eval);
setTrainingEval({
- meanSquaredError,
- rSquared,
+ mse,
+ msle,
+ huber,
+ rSquared: r_squared,
error: null,
});
setIsLoadingTraining(false);
} else {
setIsLoadingTraining(false);
setTrainingEval({
- meanSquaredError: '--',
- rSquared: '--',
+ ...EMPTY_STATS,
error: trainingErrorEval.error,
});
}
@@ -274,22 +288,48 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery })
-
+
+ {/* First row stats */}
-
+
+
+
+
+
+
+
+
+ {/* Second row stats */}
-
+
+
+
+
+
+
+
+
@@ -331,22 +371,48 @@ export const EvaluatePanel: FC = ({ jobConfig, jobStatus, searchQuery })
-
+
+ {/* First row stats */}
-
+
+
+
+
+
+
+
+
+ {/* Second row stats */}
-
+
+
+
+
+
+
+
+
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx
index 1b4461b2bb075..114ec75efb2e7 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/regression_exploration/evaluate_stat.tsx
@@ -6,58 +6,99 @@
import React, { FC } from 'react';
import { i18n } from '@kbn/i18n';
-import { EuiStat, EuiIconTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
+import { EuiStat, EuiIconTip, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui';
+import { REGRESSION_STATS } from '../../../../common/analytics';
interface Props {
isLoading: boolean;
title: number | string;
- isMSE: boolean;
+ statType: REGRESSION_STATS;
dataTestSubj: string;
}
-const meanSquaredErrorText = i18n.translate(
- 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText',
- {
- defaultMessage: 'Mean squared error',
- }
-);
-const rSquaredText = i18n.translate(
- 'xpack.ml.dataframe.analytics.regressionExploration.rSquaredText',
- {
- defaultMessage: 'R squared',
- }
-);
-const meanSquaredErrorTooltipContent = i18n.translate(
- 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorTooltipContent',
- {
- defaultMessage:
- 'Measures how well the regression analysis model is performing. Mean squared sum of the difference between true and predicted values.',
- }
-);
-const rSquaredTooltipContent = i18n.translate(
- 'xpack.ml.dataframe.analytics.regressionExploration.rSquaredTooltipContent',
- {
- defaultMessage:
- 'Represents the goodness of fit. Measures how well the observed outcomes are replicated by the model.',
- }
-);
+const statDescriptions = {
+ [REGRESSION_STATS.MSE]: i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorText',
+ {
+ defaultMessage: 'Mean squared error',
+ }
+ ),
+ [REGRESSION_STATS.MSLE]: i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.msleText',
+ {
+ defaultMessage: 'Mean squared logarithmic error',
+ }
+ ),
+ [REGRESSION_STATS.R_SQUARED]: i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.rSquaredText',
+ {
+ defaultMessage: 'R squared',
+ }
+ ),
+ [REGRESSION_STATS.HUBER]: (
+
+ {i18n.translate('xpack.ml.dataframe.analytics.regressionExploration.huberLinkText', {
+ defaultMessage: 'Pseudo Huber loss function',
+ })}
+
+ ),
+ }}
+ />
+ ),
+};
+
+const tooltipContent = {
+ [REGRESSION_STATS.MSE]: i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.meanSquaredErrorTooltipContent',
+ {
+ defaultMessage:
+ 'Measures how well the regression analysis model is performing. Mean squared sum of the difference between true and predicted values.',
+ }
+ ),
+ [REGRESSION_STATS.MSLE]: i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.msleTooltipContent',
+ {
+ defaultMessage:
+ 'Average squared difference between the logarithm of the predicted values and the logarithm of the actual (ground truth) value',
+ }
+ ),
+ [REGRESSION_STATS.R_SQUARED]: i18n.translate(
+ 'xpack.ml.dataframe.analytics.regressionExploration.rSquaredTooltipContent',
+ {
+ defaultMessage:
+ 'Represents the goodness of fit. Measures how well the observed outcomes are replicated by the model.',
+ }
+ ),
+};
-export const EvaluateStat: FC = ({ isLoading, isMSE, title, dataTestSubj }) => (
+export const EvaluateStat: FC = ({ isLoading, statType, title, dataTestSubj }) => (
-
+ {statType !== REGRESSION_STATS.HUBER && (
+
+ )}
);
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.test.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.test.ts
index 006cccf3b4610..9db32e298691e 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.test.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.test.ts
@@ -131,7 +131,7 @@ describe('Analytics job clone action', () => {
},
analyzed_fields: {
includes: [],
- excludes: [],
+ excludes: ['excluded_field'],
},
model_memory_limit: '350mb',
allow_lazy_start: false,
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx
index f8b6fdfbe2119..280ec544c1e5e 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_button.tsx
@@ -247,6 +247,7 @@ const getAnalyticsJobMeta = (config: CloneDataFrameAnalyticsConfig): AnalyticsJo
},
results_field: {
optional: true,
+ formKey: 'resultsField',
defaultValue: DEFAULT_RESULTS_FIELD,
},
},
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx
index 405231aef5774..4080f6cd7a77e 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx
@@ -39,6 +39,7 @@ import {
import { getAnalyticsFactory } from '../../services/analytics_service';
import { getTaskStateBadge, getJobTypeBadge, useColumns } from './use_columns';
import { ExpandedRow } from './expanded_row';
+import { stringMatch } from '../../../../../util/string_utils';
import {
ProgressBar,
mlInMemoryTableFactory,
@@ -65,14 +66,6 @@ function getItemIdToExpandedRowMap(
}, {} as ItemIdToExpandedRowMap);
}
-function stringMatch(str: string | undefined, substr: any) {
- return (
- typeof str === 'string' &&
- typeof substr === 'string' &&
- (str.toLowerCase().match(substr.toLowerCase()) === null) === false
- );
-}
-
const MlInMemoryTable = mlInMemoryTableFactory();
interface Props {
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx
index 4d029ff1d9546..5276fedff0fde 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/expanded_row.tsx
@@ -29,6 +29,7 @@ import { getDataFrameAnalyticsProgressPhase, isCompletedAnalyticsJob } from './c
import {
isRegressionAnalysis,
ANALYSIS_CONFIG_TYPE,
+ REGRESSION_STATS,
isRegressionEvaluateResponse,
} from '../../../../common/analytics';
import { ExpandedRowMessagesPane } from './expanded_row_messages_pane';
@@ -44,7 +45,7 @@ function getItemDescription(value: any) {
interface LoadedStatProps {
isLoading: boolean;
evalData: Eval;
- resultProperty: 'meanSquaredError' | 'rSquared';
+ resultProperty: REGRESSION_STATS;
}
const LoadedStat: FC = ({ isLoading, evalData, resultProperty }) => {
@@ -61,7 +62,7 @@ interface Props {
item: DataFrameAnalyticsListRow;
}
-const defaultEval: Eval = { meanSquaredError: '', rSquared: '', error: null };
+const defaultEval: Eval = { mse: '', msle: '', huber: '', rSquared: '', error: null };
export const ExpandedRow: FC = ({ item }) => {
const [trainingEval, setTrainingEval] = useState(defaultEval);
@@ -94,17 +95,21 @@ export const ExpandedRow: FC = ({ item }) => {
genErrorEval.eval &&
isRegressionEvaluateResponse(genErrorEval.eval)
) {
- const { meanSquaredError, rSquared } = getValuesFromResponse(genErrorEval.eval);
+ const { mse, msle, huber, r_squared } = getValuesFromResponse(genErrorEval.eval);
setGeneralizationEval({
- meanSquaredError,
- rSquared,
+ mse,
+ msle,
+ huber,
+ rSquared: r_squared,
error: null,
});
setIsLoadingGeneralization(false);
} else {
setIsLoadingGeneralization(false);
setGeneralizationEval({
- meanSquaredError: '',
+ mse: '',
+ msle: '',
+ huber: '',
rSquared: '',
error: genErrorEval.error,
});
@@ -124,17 +129,21 @@ export const ExpandedRow: FC = ({ item }) => {
trainingErrorEval.eval &&
isRegressionEvaluateResponse(trainingErrorEval.eval)
) {
- const { meanSquaredError, rSquared } = getValuesFromResponse(trainingErrorEval.eval);
+ const { mse, msle, huber, r_squared } = getValuesFromResponse(trainingErrorEval.eval);
setTrainingEval({
- meanSquaredError,
- rSquared,
+ mse,
+ msle,
+ huber,
+ rSquared: r_squared,
error: null,
});
setIsLoadingTraining(false);
} else {
setIsLoadingTraining(false);
setTrainingEval({
- meanSquaredError: '',
+ mse: '',
+ msle: '',
+ huber: '',
rSquared: '',
error: genErrorEval.error,
});
@@ -221,7 +230,17 @@ export const ExpandedRow: FC = ({ item }) => {
+ ),
+ },
+ {
+ title: 'generalization mean squared logarithmic error',
+ description: (
+
),
},
@@ -231,7 +250,17 @@ export const ExpandedRow: FC = ({ item }) => {
+ ),
+ },
+ {
+ title: 'generalization pseudo huber loss function',
+ description: (
+
),
},
@@ -241,7 +270,17 @@ export const ExpandedRow: FC = ({ item }) => {
+ ),
+ },
+ {
+ title: 'training mean squared logarithmic error',
+ description: (
+
),
},
@@ -251,7 +290,17 @@ export const ExpandedRow: FC = ({ item }) => {
+ ),
+ },
+ {
+ title: 'training pseudo huber loss function',
+ description: (
+
),
}
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx
index e75d938116991..cb46a88fa3b21 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_actions.tsx
@@ -30,17 +30,21 @@ export const useActions = (
actions: EuiTableActionsColumnType['actions'];
modals: JSX.Element | null;
} => {
- const deleteAction = useDeleteAction();
- const editAction = useEditAction();
- const startAction = useStartAction();
-
let modals: JSX.Element | null = null;
const actions: EuiTableActionsColumnType['actions'] = [
getViewAction(isManagementTable),
];
+ // isManagementTable will be the same for the lifecycle of the component
+ // Disabling lint error to fix console error in management list due to action hooks using deps not initialized in management
if (isManagementTable === false) {
+ /* eslint-disable react-hooks/rules-of-hooks */
+ const deleteAction = useDeleteAction();
+ const editAction = useEditAction();
+ const startAction = useStartAction();
+ /* eslint-disable react-hooks/rules-of-hooks */
+
modals = (
<>
{startAction.isModalVisible && }
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts
index 81d35679443b8..b344e44c97d59 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/reducer.ts
@@ -144,6 +144,11 @@ export const validateAdvancedEditor = (state: State): State => {
const destinationIndexNameValid = isValidIndexName(destinationIndexName);
const destinationIndexPatternTitleExists =
state.indexPatternsMap[destinationIndexName] !== undefined;
+
+ const resultsFieldEmptyString =
+ typeof jobConfig?.dest?.results_field === 'string' &&
+ jobConfig?.dest?.results_field.trim() === '';
+
const mml = jobConfig.model_memory_limit;
const modelMemoryLimitEmpty = mml === '' || mml === undefined;
if (!modelMemoryLimitEmpty && mml !== undefined) {
@@ -292,6 +297,18 @@ export const validateAdvancedEditor = (state: State): State => {
});
}
+ if (resultsFieldEmptyString) {
+ state.advancedEditorMessages.push({
+ error: i18n.translate(
+ 'xpack.ml.dataframe.analytics.create.advancedEditorMessage.resultsFieldEmptyString',
+ {
+ defaultMessage: 'The results field must not be an empty string.',
+ }
+ ),
+ message: '',
+ });
+ }
+
if (dependentVariableEmpty) {
state.advancedEditorMessages.push({
error: i18n.translate(
@@ -336,6 +353,7 @@ export const validateAdvancedEditor = (state: State): State => {
sourceIndexNameValid &&
!destinationIndexNameEmpty &&
destinationIndexNameValid &&
+ !resultsFieldEmptyString &&
!dependentVariableEmpty &&
!modelMemoryLimitEmpty &&
numTopFeatureImportanceValuesValid &&
diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts
index cedbe9094cb20..0d425c8ead4a2 100644
--- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts
+++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/state.ts
@@ -82,6 +82,7 @@ export interface State {
previousJobType: null | AnalyticsJobType;
requiredFieldsError: string | undefined;
randomizeSeed: undefined | number;
+ resultsField: undefined | string;
sourceIndex: EsIndexName;
sourceIndexNameEmpty: boolean;
sourceIndexNameValid: boolean;
@@ -147,6 +148,7 @@ export const getInitialState = (): State => ({
previousJobType: null,
requiredFieldsError: undefined,
randomizeSeed: undefined,
+ resultsField: undefined,
sourceIndex: '',
sourceIndexNameEmpty: true,
sourceIndexNameValid: false,
@@ -198,6 +200,13 @@ export const getJobConfigFromFormState = (
model_memory_limit: formState.modelMemoryLimit,
};
+ const resultsFieldEmpty =
+ typeof formState?.resultsField === 'string' && formState?.resultsField.trim() === '';
+
+ if (jobConfig.dest && !resultsFieldEmpty) {
+ jobConfig.dest.results_field = formState.resultsField;
+ }
+
if (
formState.jobType === ANALYSIS_CONFIG_TYPE.REGRESSION ||
formState.jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION
@@ -277,6 +286,7 @@ export function getCloneFormStateFromJobConfig(
const resultState: Partial = {
jobType,
description: analyticsJobConfig.description ?? '',
+ resultsField: analyticsJobConfig.dest.results_field,
sourceIndex: Array.isArray(analyticsJobConfig.source.index)
? analyticsJobConfig.source.index.join(',')
: analyticsJobConfig.source.index,
diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts
index 7d1f456d2334f..a08821c65bfe7 100644
--- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts
+++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts
@@ -10,13 +10,12 @@ import { getToastNotifications } from '../../../util/dependency_cache';
import { IndexPattern } from '../../../../../../../../src/plugins/data/public';
import { SavedSearchQuery } from '../../../contexts/ml';
+import { OMIT_FIELDS } from '../../../../../common/constants/field_types';
import { IndexPatternTitle } from '../../../../../common/types/kibana';
import { ml } from '../../../services/ml_api_service';
import { FieldRequestConfig } from '../common';
-// List of system fields we don't want to display.
-const OMIT_FIELDS: string[] = ['_source', '_type', '_index', '_id', '_version', '_score'];
// Maximum number of examples to obtain for text type fields.
const MAX_EXAMPLES_DEFAULT: number = 10;
diff --git a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts
index 068f43a140c90..f356d79c0a8e1 100644
--- a/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts
+++ b/x-pack/plugins/ml/public/application/explorer/hooks/use_selected_cells.ts
@@ -9,12 +9,10 @@ import { useUrlState } from '../../util/url_state';
import { SWIMLANE_TYPE } from '../explorer_constants';
import { AppStateSelectedCells } from '../explorer_utils';
-export const useSelectedCells = (): [
- AppStateSelectedCells | undefined,
- (swimlaneSelectedCells: AppStateSelectedCells) => void
-] => {
- const [appState, setAppState] = useUrlState('_a');
-
+export const useSelectedCells = (
+ appState: any,
+ setAppState: ReturnType